Как создать кастомную аутентификацию в Laravel
В этой статье мы рассмотрим систему аутентификации в рамках Laravel. Основная цель этой статьи — создать настраиваемый защитный механизм аутентификации путем расширения базовой системы аутентификации.
Laravel в своем ядре обеспечивает очень прочную систему аутентификации, что делает внедрение базовой аутентификации совсем простым. На самом деле вам просто нужно запустить пару artisan команд, чтобы настроить каркас системы аутентификации.
Более того, сама система разработана таким образом, что вы можете ее расширить и подключить к своим адаптерам аутентификации. Это то, что мы подробно обсудим в этой статье. Прежде чем мы перейдем к внедрению специального средства проверки подлинности, мы начнем с обсуждения основных элементов аутентификации и ее провайдеров в Laravel.
Основные элементы: гуарды и провайдеры
Система аутентификации Laravel состоит из двух элементов в своем ядре: гуарды и провайдеры.
Гуарды
Гуард можно рассматривать как способе подачи логики, которая используется для идентификации пользователей, прошедших проверку аутентификации. В основе Laravel предоставляет различные гуарды, такие как сессия и токен. Гуард сессии поддерживает состояние пользователя в каждом запросе с помощью файлов cookie, а с другой стороны гуард токена аутентифицирует пользователя, проверяя действительный токен в каждом запросе.
Итак, как вы можете видеть, гуард определяет логику аутентификации, и нет необходимости, чтобы он всегда справлялся с этим, выбирая действительные учетные данные из вашего бэкенда. Вы можете реализовать гуард, который просто проверяет наличие определенной вещи в заголовках запросов и аутентифицирует пользователей на основе этого.
Позже в этой статье мы будем внедрять гуард, который проверяет определенные параметры JSON в заголовках запроса и извлекает валидного пользователя из MongoDB.
Провайдеры
Если гуард определяет логику аутентификации, поставщик аутентификации отвечает за извлечение пользователя из внутреннего хранилища. Если гуард требует, чтобы пользователь был проверен против внутреннего хранилища, тогда реализация запроса пользователя переходит в поставщика проверки подлинности.
Laravel поставляется с двумя поставщиками аутентификации по умолчанию — Database and Eloquent. Поставщик аутентификации базы данных имеет дело с прямым поиском учетных данных пользователя из внутреннего хранилища, в то время как Eloquent предоставляет уровень абстракции, который делает это необходимым.
В нашем примере мы реализуем поставщика аутентификации MongoDB, который извлекает учетные данные пользователя MongoDB.
Итак, это было базовое введение в гуардов и провайдеров в системе аутентификации Laravel. В следующем разделе мы сосредоточимся на разработке кастомных гуарда и поставщика!
Быстрый взгляд на настройку файлов
Давайте быстро рассмотрим список файлов, которые мы будем использовать на протяжении этой статьи.
config/auth.php
: Это файл конфигурации аутентификации, в который мы добавим запись нашего пользовательского гуарда.config/mongo.php
: Это файл, содержащий конфигурацию MongoDB.app/Services/Contracts/NosqlServiceInterface.php
: Это интерфейс, который реализует наш пользовательский класс базы данных Mongo.app/Database/MongoDatabase.php
: Это основной класс базы данных, который взаимодействует с MongoDB.app/Models/Auth/User.php
: Это класс модели пользователя, который реализует контракт Authenticable.app/Extensions/MongoUserProvider.php
: Это реализация поставщика проверки подлинности.app/Services/Auth/JsonGuard.php
: Это реализация драйвера гуарда аутентификации.app/Providers/AuthServiceProvider.php
: Это уже существующий файл, который мы будем использовать для добавления привязок контейнера нашего сервиса.app/Http/Controllers/MongoController.php
: Это файл демонстрационного контроллера, который мы будем создавать, чтобы протестировать наш пользовательский гуард.
Не беспокойтесь, если данный список файлов пока для вас не поняет, поскольку скоро мы подробно обсудим его.
Погружение в реализацию
В этом разделе мы рассмотрим реализацию необходимых файлов.
Первое, что нам нужно сделать, это сообщить Laravel о нашем кастомном гуарде. Идем дальше и добавляем данные кастомного гуарда в файл config/auth.php,
как показано ниже.
...
...
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
'custom' => [
'driver' => 'json',
'provider' => 'mongo',
],
],
...
...
Как вы можете видеть, мы добавили наш кастомный гуард под специальным ключом custom.
Затем нам нужно добавить соответствующую запись провайдера в разделе providers.
...
...
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
'mongo' => [
'driver' => 'mongo'
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
...
...
Мы добавили запись нашего провайдера под ключом mongo.
Наконец, давайте изменим гуард аутентификации по умолчанию с web на custom.
...
...
'defaults' => [
'guard' => 'custom',
'passwords' => 'users',
],
...
...
Конечно, пока это не сработает, поскольку мы еще не реализовали необходимые файлы. И об этом мы поговорим в следующих разделах.
Настройка драйвера MongoDB
В этом разделе мы будем реализовывать необходимые файлы, которые общаются с базовым экземпляром MongoDB.
Сначала создадим конфигурационный файл config/mongo.php
, который содержит настройки соединения MongoDB по умолчанию.
<?php
return [
'defaults' => [
'host' => '{HOST_IP}',
'port' => '{HOST_PORT}',
'database' => '{DB_NAME}'
]
];
Конечно, вам нужно изменить значения заполнителя в соответствии с вашими настройками.
Вместо прямого создания класса, который взаимодействует с MongoDB, мы в первую очередь создадим интерфейс.
Преимущество создания интерфейса заключается в том, что он предоставляет контракт, которого разработчик должен придерживаться при его реализации. Кроме того, наша реализация MongoDB может быть легко заменена другой версией NoSQL, если это необходимо.
Идем дальше и создаем файла интерфейса app/Services/Contracts/NosqlServiceInterface.php
со следующим содержимым.
<?php
// app/Services/Contracts/NosqlServiceInterface.php
namespace App\Services\Contracts;
Interface NosqlServiceInterface
{
/**
* Create a Document
*
* @param string $collection Collection/Table Name
* @param array $document Document
* @return boolean
*/
public function create($collection, Array $document);
/**
* Update a Document
*
* @param string $collection Collection/Table Name
* @param mix $id Primary Id
* @param array $document Document
* @return boolean
*/
public function update($collection, $id, Array $document);
/**
* Delete a Document
*
* @param string $collection Collection/Table Name
* @param mix $id Primary Id
* @return boolean
*/
public function delete($collection, $id);
/**
* Search Document(s)
*
* @param string $collection Collection/Table Name
* @param array $criteria Key-value criteria
* @return array
*/
public function find($collection, Array $criteria);
}
Это довольно простой интерфейс, который объявляет базовые методы CRUD, которые должен определить класс, реализующий этот интерфейс.
Теперь давайте определим фактический класс в app/Database/MongoDatabase.php
.
<?php
// app/Database/MongoDatabase.php
namespace App\Database;
use App\Services\Contracts\NosqlServiceInterface;
class MongoDatabase implements NosqlServiceInterface
{
private $connection;
private $database;
public function __construct($host, $port, $database)
{
$this->connection = new MongoClient( "mongodb://{$host}:{$port}" );
$this->database = $this->connection->{$database};
}
/**
* @see \App\Services\Contracts\NosqlServiceInterface::find()
*/
public function find($collection, Array $criteria)
{
return $this->database->{$collection}->findOne($criteria);
}
public function create($collection, Array $document) {}
public function update($collection, $id, Array $document) {}
public function delete($collection, $id) {}
}
Конечно, я предполагаю, что вы установили MongoDB и соответствующее расширение PHP MongoDB.
Метод __construct
создает экземпляр класса MongoClient
с необходимыми параметрами. Другим важным методом, который нас интересует, является метод find
, который извлекает запись на основе критериев, предоставленных в качестве аргументов метода.
Такого была реализация драйвера MongoDB, и я старался максимально упростить его.
Настройка модели пользователя
Соблюдая стандарты системы аутентификации, нам необходимо реализовать модель User, которая должна реализовать контракт Illuminate\Contracts\Auth\Authenticatable
.
Двигаемся дальше и создаем файл app/Models/Auth/User.php
со следующим содержимым.
<?php
// app/Models/Auth/User.php
namespace App\Models\Auth;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use App\Services\Contracts\NosqlServiceInterface;
class User implements AuthenticatableContract
{
private $conn;
private $username;
private $password;
protected $rememberTokenName = 'remember_token';
public function __construct(NosqlServiceInterface $conn)
{
$this->conn = $conn;
}
/**
* Fetch user by Credentials
*
* @param array $credentials
* @return Illuminate\Contracts\Auth\Authenticatable
*/
public function fetchUserByCredentials(Array $credentials)
{
$arr_user = $this->conn->find('users', ['username' => $credentials['username']]);
if (! is_null($arr_user)) {
$this->username = $arr_user['username'];
$this->password = $arr_user['password'];
}
return $this;
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifierName()
*/
public function getAuthIdentifierName()
{
return "username";
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifier()
*/
public function getAuthIdentifier()
{
return $this->{$this->getAuthIdentifierName()};
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getAuthPassword()
*/
public function getAuthPassword()
{
return $this->password;
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getRememberToken()
*/
public function getRememberToken()
{
if (! empty($this->getRememberTokenName())) {
return $this->{$this->getRememberTokenName()};
}
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::setRememberToken()
*/
public function setRememberToken($value)
{
if (! empty($this->getRememberTokenName())) {
$this->{$this->getRememberTokenName()} = $value;
}
}
/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getRememberTokenName()
*/
public function getRememberTokenName()
{
return $this->rememberTokenName;
}
}
Вы должны были заметить, что App\Models\Auth\User
реализует контракт Illuminate\Contracts\Auth\Authenticatable
.
Большинство методов, реализованных в нашем классе, не требуют пояснений. Имея это в виду, мы определили метод fetchUserByCredentials
, который извлекает пользователя. В нашем случае это будет класс MongoDatabase
, который будет вызываться для извлечения необходимой информации.
Итак, это реализация модели User.
Настройка провайдера аутентификации
Как мы обсуждали ранее, система аутентификации Laravel состоит из двух элементов — гуардов и провайдеров.
В этом разделе мы создадим провайдера аутентификации, который занимается поиском пользователя на бэкенде.
Двигаемся дальше и создаем файл app/Extensions/MongoUserProvider.php
, как показано ниже.
<?php
// app/Extensions/MongoUserProvider.php
namespace App\Extensions;
use Illuminate\Support\Str;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
class MongoUserProvider implements UserProvider
{
/**
* The Mongo User Model
*/
private $model;
/**
* Create a new mongo user provider.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
* @return void
*/
public function __construct(\App\Models\Auth\User $userModel)
{
$this->model = $userModel;
}
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials)) {
return;
}
$user = $this->model->fetchUserByCredentials(['username' => $credentials['username']]);
return $user;
}
/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param array $credentials Request credentials
* @return bool
*/
public function validateCredentials(Authenticatable $user, Array $credentials)
{
return ($credentials['username'] == $user->getAuthIdentifier() &&
md5($credentials['password']) == $user->getAuthPassword());
}
public function retrieveById($identifier) {}
public function retrieveByToken($identifier, $token) {}
public function updateRememberToken(Authenticatable $user, $token) {}
}
Опять же, вам нужно убедиться, что пользовательский провайдер должен реализовать контракт Illuminate\Contracts\Auth\UserProvider
.
Двигаясь вперед, он определяет два важных метода: retrieveByCredentials и validateCredentials.
Метод retrieveByCredentials
используется для извлечения учетных данных пользователя с использованием класса модели User, который обсуждался в предыдущем разделе. С другой стороны, метод validateCredentials
используется для проверки пользователя с использованием данного набора учетных данных.
И это была реализация нашего пользовательского провайдера аутентификации. В следующем разделе мы продолжим работу и создаем гуарда, который взаимодействует с провайдеров аутентификации MongoUserProvider
.
Настройка гуарда аутентификации
Как мы обсуждали ранее, гуард в системе аутентификации Laravel устанавливает, как пользователь аутентифицируется. В нашем случае мы проверим наличие параметра запроса jsondata, который должен содержать строку учетных данных, кодированную JSON.
В этом разделе мы создадим гуарда, который взаимодействует с провайдеров аутентификации, который был только что создан в последнем разделе.
Далее создайте файл app/Services/Auth/JsonGuard.php
со следующим содержимым.
<?php
// app/Services/Auth/JsonGuard.php
namespace App\Services\Auth;
use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use GuzzleHttp\json_decode;
use phpDocumentor\Reflection\Types\Array_;
use Illuminate\Contracts\Auth\Authenticatable;
class JsonGuard implements Guard
{
protected $request;
protected $provider;
protected $user;
/**
* Create a new authentication guard.
*
* @param \Illuminate\Contracts\Auth\UserProvider $provider
* @param \Illuminate\Http\Request $request
* @return void
*/
public function __construct(UserProvider $provider, Request $request)
{
$this->request = $request;
$this->provider = $provider;
$this->user = NULL;
}
/**
* Determine if the current user is authenticated.
*
* @return bool
*/
public function check()
{
return ! is_null($this->user());
}
/**
* Determine if the current user is a guest.
*
* @return bool
*/
public function guest()
{
return ! $this->check();
}
/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
if (! is_null($this->user)) {
return $this->user;
}
}
/**
* Get the JSON params from the current request
*
* @return string
*/
public function getJsonParams()
{
$jsondata = $this->request->query('jsondata');
return (!empty($jsondata) ? json_decode($jsondata, TRUE) : NULL);
}
/**
* Get the ID for the currently authenticated user.
*
* @return string|null
*/
public function id()
{
if ($user = $this->user()) {
return $this->user()->getAuthIdentifier();
}
}
/**
* Validate a user's credentials.
*
* @return bool
*/
public function validate(Array $credentials=[])
{
if (empty($credentials['username']) || empty($credentials['password'])) {
if (!$credentials=$this->getJsonParams()) {
return false;
}
}
$user = $this->provider->retrieveByCredentials($credentials);
if (! is_null($user) && $this->provider->validateCredentials($user, $credentials)) {
$this->setUser($user);
return true;
} else {
return false;
}
}
/**
* Set the current user.
*
* @param Array $user User info
* @return void
*/
public function setUser(Authenticatable $user)
{
$this->user = $user;
return $this;
}
}
Прежде всего, нашему классу необходимо реализовать интерфейс Illuminate\Contracts\Auth\Guard
. Таким образом, нам нужно определить все методы, объявленные в этом интерфейсе.
Важно отметить, что функция __construct
требует реализации Illuminate\Contracts\Auth\UserProvider
. В нашем случае мы передадим экземпляр App\Extensions\MongoUserProvider
.
Далее, есть функция getJsonParams
, которая извлекает учетные данные пользователя из параметра запроса с именем jsondata
. Поскольку ожидается, что мы получим кодированную строку JSON учетных данных пользователя, мы использовали функцию json_decode
для декодирования данных JSON.
В функции validate первое, что мы проверяем, это наличие аргумента $credentials
. Если этого нет, мы вызываем метод getJsonParams
для получения учетных данных пользователя из параметров запроса.
Затем мы вызываем метод retrieveByCredentials
провайдера MongoUserProvider
, который извлекает пользователя из базы данных MongoDB. Наконец, метод validateCredentials
провайдера MongoUserProvider
проверяет валидность User.
Итак, это была реализация нашего кастомного гуарда. В следующем разделе описывается, как соединить эти фрагменты вместе, чтобы сформировать успешную систему аутентификации.
Все вместе
До сих пор мы разработали все элементы кастомного гуарда аутентификации, которые должны предоставить нам новую систему аутентификации. Однако он не будет работать из коробки, поскольку мы должны сначала зарегистрировать его, используя привязки сервисов в контейнере Laravel.
Как вы уже должны знать, провайдер услуг Laravel является правильным местом для реализации необходимых привязок.
Откройте файл app/Providers/AuthServiceProvider.php
, который позволяет нам добавлять привязки гуардов. Если он не содержит каких-либо пользовательских изменений, вы можете просто заменить его следующим содержимым.
<?php
// app/Providers/AuthServiceProvider.php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use App\Services\Auth\JsonGuard;
use App\Extensions\MongoUserProvider;
use App\Database\MongoDatabase;
use App\Models\Auth\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
$this->app->bind('App\Database\MongoDatabase', function ($app) {
return new MongoDatabase(config('mongo.defaults.host'), config('mongo.defaults.port'), config('mongo.defaults.database'));
});
$this->app->bind('App\Models\Auth\User', function ($app) {
return new User($app->make('App\Database\MongoDatabase'));
});
// add custom guard provider
Auth::provider('mongo', function ($app, array $config) {
return new MongoUserProvider($app->make('App\Models\Auth\User'));
});
// add custom guard
Auth::extend('json', function ($app, $name, array $config) {
return new JsonGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
});
}
public function register()
{
$this->app->bind(
'App\Services\Contracts\NosqlServiceInterface',
'App\Database\MongoDatabase'
);
}
}
Давайте рассмотрим метод boot
, который содержит большинство привязок провайдера.
Для начала создадим привязки для элементов App\Database\MongoDatabase
и App\Models\Auth\User
.
$this->app->bind('App\Database\MongoDatabase', function ($app) {
return new MongoDatabase(config('mongo.defaults.host'), config('mongo.defaults.port'), config('mongo.defaults.database'));
});
$this->app->bind('App\Models\Auth\User', function ($app) {
return new User($app->make('App\Database\MongoDatabase'));
});
Прошло некоторое время, когда мы говорили о провайдере и охранниках, и пришло время подключить наш собственный гуард к системе аутентификации Laravel.
Мы использовали метод провайдера Auth
Facade для добавления нашего кастомного гуадра аутентификации по ключу mongo. Напомним, что ключ отражает параметры, которые были добавлены ранее в файл auth.php
.
Auth::provider('mongo', function ($app, array $config) {
return new MongoUserProvider($app->make('App\Models\Auth\User'));
});
Аналогичным образом мы будем внедрять наш кастомный гуард, используя метод extend фасада Auth
.
Auth::extend('json', function ($app, $name, array $config) {
return new JsonGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
});
Далее, существует метод register
, который мы использовали для привязки интерфейса App\Services\Contracts\NosqlServiceInterface
к реализации App\Database\MongoDatabase
.
$this->app->bind(
'App\Services\Contracts\NosqlServiceInterface',
'App\Database\MongoDatabase'
);
Поэтому всякий раз, когда возникает необходимость в разрешении зависимости App\Services\Contracts\NosqlServiceInterface
, Laravel отвечает реализацией адаптера App\Database\MongoDatabase
.
Преимущество использования этого подхода заключается в том, что можно легко подменить данную реализацию своей кастомной. Например, скажем, кто-то хотел бы заменить реализацию App\Database\MongoDatabase
адаптером CouchDB в будущем. В этом случае им просто нужно добавить соответствующую привязку в методе register.
Так что это был поставщик услуг в вашем распоряжении. На данный момент у нас есть все, что требуется для проверки нашей реализации пользовательского гуарда, поэтому следующий и заключительный раздел будет об этом.
А это вообще работает?
Вы проделали всю тяжелую работу, настроив свой первый кастомный гуард аутентификации, и теперь пришло время воспользоваться его преимуществами.
Давайте быстро реализуем простой базовый контроллер app/Http/Controllers/MongoController.php
, как показано ниже.
<?php
// app/Http/Controllers/MongoController.php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Guard;
class MongoController extends Controller
{
public function login(Guard $auth_guard)
{
if ($auth_guard->validate()) {
// get the current authenticated user
$user = $auth_guard->user();
echo 'Success!';
} else {
echo 'Not authorized to access this page!';
}
}
}
Внимательно посмотрите на зависимость метода login в систему, для чего требуется реализация гуарда Illuminate\Contracts\Auth\Guard
. Поскольку в файле auth.php установлен custom
гуард в качестве гуарда по умолчанию, то будет использоваться файл App\Services\Auth\JsonGuard
.
Затем мы вызываем метод validate
класса App\Services\Auth\JsonGuard
, который, в свою очередь, инициирует серию вызовов методов:
- Он вызывает метод
retrieveByCredentials
классаApp\Extensions\MongoUserProvider
. - Метод
retrieveByCredentials
вызывает методfetchUserByCredentials
класса UserApp\Models\Auth\User
. - Метод
fetchUserByCredentials
вызывает методfind
изApp\Database\MongoDatabase
для получения учетных данных пользователя. - Наконец, метод
find
изApp\Database\MongoDatabase
возвращает ответ!
Если все работает так, как ожидалось, мы должны получить аутентифицированного пользователя, вызвав метод user
нашего гуарда.
Чтобы получить доступ к контроллеру, вы должны добавить соответствующий маршрут в файле routes/web.php
.
Route::get('/custom/mongo/login', 'MongoController@login');
Попробуйте получить доступ к URL-адресу http://your-laravel-site/custom/mongo/login, не передавая никаких параметров, и вы должны увидеть сообщение «не авторизован».
С другой стороны, попробуйте что-то вроде http://your-laravel-site/custom/mongo/login?jsondata={«username»:»admin», «password»: «admin»}, и это должно вернуть сообщение об успешном завершении если пользователь присутствует в вашей базе данных.
Обратите внимание, что так сделано просто для примера, чтобы продемонстрировать, как работает пользовательский гуард. Вы должны реализовать надежное решение для такой функции, как логин. Фактически, я только что дал представление об потоке аутентификации; вы несете ответственность за создание надежного и безопасного решения для вашего приложения.
На этом наше путешествие сегодня заканчивается, и, надеюсь, что я вернусь с более полезными вещами. Если вы хотите, чтобы я писал на какие-либо конкретные темы, не забудьте оставить мне сообщение!
Заключение
Фреймворк Laravel обеспечивает надежную систему аутентификации, которая может быть расширена, если вы хотите реализовать свою кастомную. Это была тема сегодняшней статьи по внедрению пользовательского гуарда и подключению его к рабочему процессу аутентификации Laravel.
В ходе этого мы продолжили работу и разработали систему, которая аутентифицирует пользователя на основе JSON данных в запросе и сопоставляет его с базой данных MongoDB. И для этого мы создали кастомный гуард защиту и реализацию пользовательского провайдера.
Я надеюсь, что это упражнение предоставило вам понимание потока аутентификации Laravel, и теперь вы должны чувствовать себя в своих собственных действиях более уверенно.
Для тех из вас, кто только начинает работать с Laravel или хочет расширить свои знания, создать сайт или приложение с расширениями, у нас есть множество вещей, которые вы можете изучить на Envato Market.
Я хотел бы услышать ваши отзывы и предложения, поэтому кричите громко, используя приведенный ниже канал связи!