Laravel - MVC პრინციპზე დაფუძნებული პლატფორმა, ფრეიმვორკი, ინსტრუმენტთა ნაკრები იმ პროგრამისტებისათვის, რომლებიც PHP-ის მეშვეობით ამზადებენ ვებ-გვერდებს. ფრეიმვორკის დახმარებით გაცილებით სწრაფად და მარტივად ვქმნით აპლიკაციებს და აღარ გვიწევს კოდის წერის დაწყება ნოლიდან. ფაქტიურად ფრეიმვორკი წარმოადგენს მომავალი პროექტის კარკასს, ჩონჩხს, მასში ჩადებულია ისეთი კლასები რომელთა მეშვეობითაც პროგრამისტს აღარ უწევს მთელი რიგი სტანდარტული და რუტინული სამუშაოების ჩატარება, მაგალითად: საიტზე შესვლის წერტილის შექმნა, ადამიანისათვის გასაგები URL-ების შექმნა, შემავალი პარამეტრებისა და მონაცემების ვალიდაციის მექანიზმის შექმნა, მონაცემთა ბაზასთან მუშაობის ორგანიზება და ა.შ
Laravel-ის პირველი ვერსია შეიქმნა 2011 წელს (ავტ. Taylor Otwell).
დიდი ალბათობით, Laravel-ის შესწავლისას მომხმარებელს უკვე ექნება შეხება PHP-ს რომელიმე ინტერპრეტატორთან და მონაცემთა ბაზის სერვერთან, ეს შეიძლება იყოს როგორც ცალკე დაინსტალირებული პროგრამები, მაგ: ვებ-სერვერი Apache, PHP ინტერპრეტატორი, მბ სერვერი MYSQL, ასევე გამზადებული ნაკრებები მაგ: Denver, OpenServer, XAMPP, WAMP, LAMP და ა.შ. შესაძლებელია მათი გამოყენებაც.
ინტალაციის პროცესი მოგვთხოვს გზას PHP-მდე, რომელიც შეიძლება გამოიყურებოდეს ასე:
composer
თუ შედეგად ვიხილავ ამდაგვარ სურათს:
composer create-project <PACKAGE_NAME> <MY_PROJECT>
ჩნდება კითხვები : მაინც სად არის ინახება ეს პაკეტები ? საიდან მოაქვს Composer-ს ეს ყველაფერი ?
როდესაც Composer-ი ინსტალირდება, ნაგულისხმეობის პრინციპით ხდება ერთადერთი საცავის რეგისტრაცია, ეს საცავია ვებ-გვერდი Packagist.org.
composer create-project laravel/laravel laravel
ბრძანების გაშვების შემდეგ დაიწყება Laravel-ის ინსტალაცია და აგრეთვე შეიქმნება laravel საქაღალდე შესაბამისი ფაილებისა და საქაღალდეების სტრუქტურით :
Artisan-ი არის Laravel-ში ჩადგმული ბრძანებათა ინტერფეისის სახელწოდება, რომლის მეშვეობითაც შესაძლებელია სხვადასხვა საჭირო ბრძანებებისა და ინსტრუქციების საკმაოდ მარტივად გაშვება აპლიკაციაზე მუშობის პროცესში. Artisan ბრძანებათა სრული ჩამონათვალის სანახავად უნდა გავუშვათ შემდეგი ბრძანება :
php artisan list
php artisan serve
შედეგი იქნება :
ამ საქაღალდეში აგრეთვე მოთავსებულია კატალოგი cache, სადაც თავმოყრილი ფაილები გამოიყენება ფრეიმვორკის მუშაობის დროს მიმდინარე პროცესების ოპტიმიზაციისათვის (მაგ: მარშრუტიზაცია, ფაილთა ქეშირება)
php artisan app:name <name-of-your-application>
პროექტის კეთების ეტაპი შესაძლებელია დავყოთ სამ ნაწილად:
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:G1LDmxoK8uRi9uWtKO0ae4TQgLmTN7VEJszlIncU1BA=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
გარემოს ცვლადების მნიშვნელობები მოქცეულია სუპერგლობალურ ცვლადში - $_ENV. მნიშვნელობათა განსაზღვრა შესაძლებელია env() დამხმარე ფუნქციის მეშვეობითაც, რომელსაც პირველ არგუმენტად უნდა გადაეცეს პარამეტრის დასახელება, მეორე არგუმენტად კი შეგვიძლია გადავცეთ ნაგულისმები მნიშვნელობა იმ შემთხვევისათვის თუ ფაილში ვერ მოიძებნება მითითებული გასაღების შესაბამისი პარამეტრი.
შესაძლებელია, რომ .env ფაილში აღვწეროთ ჩვენი საკუთარი პარამეტრებიც. თუ პარამეტრის მნიშვნელობა შეიცავს გამოტოვებულ ადგილებს მაშინ მნიშვნელობა უნდა ჩაისვას ორმაგ ბრჭყალებში:
...
CUSTOM_PARAM="CUSTOM PARAM VALUE"
...
მთავარი გვერდის მარშრუტის დამმუშვებელი ფუნქცია გადავაკეთოთ შემდეგნაირად :
Route::get('/', function () {
echo '<pre>';
print_r($_ENV);
echo '</pre>';
echo '<pre>';
print_r($_ENV['APP_DEBUG']);
echo '</pre>';
echo '<pre>';
print_r(env('SESSION_LIFETIME'));
echo '</pre>';
echo '<pre>';
print_r(env('CUSTOM_PARAM'));
echo '</pre>';
echo '<pre>';
print_r(env('UNKNOWN_PARAM','UNKNOWN PARAM VALUE'));
echo '</pre>';
});
თუ ახლა შევალთ შემდეგ მისამართზე : http://127.0.0.1:8000/, ვიხილავთ შემდეგ სურათს :
'name' => env('APP_NAME', 'Laravel'),
რომლის მნიშვნელობაც ბრუნდება env ფუნქციის მეშვეობით, ეს ფუნქცია ინფორმაციას იღებს .env ფაილიდან, და ამ ინფორმაციას უტოლებს მასივის
გასაღებებს, მას აგრეთვე მითითებული აქვს მეორე პარამეტრიც, რომელიც გამოიყენება ნაგულისხმეობის პრინციპით იმ შემთხვევაში თუ .env ფაილში
ასეთი კონფიგურაციული პარამეტრი არ არის განსაზღვრული.
ეს ყველაფერი რომ უფრო კარგად გავიგოთ განვიხილოთ მასივის სხვა ელემენტიც:
'debug' => env('APP_DEBUG', false),
როგორც ვიცით, "debug" კონფიგურაციული პარამეტრი განსაზღვრავს გამოჩნდეს თუ არა დაშვებული შეცდომების შესახებ შეტყობინებები. თუ .env ფაილში
არ არის მითითებული შესაბამისი მნიშვნელობა, მაშინ სისტემა ამ პარამეტრს მიანიჭებს მნიშვნელობას - false.
მასივის ერთ-ერთი გასაღები არის შემდეგი :
'url' => env('APP_URL', 'http://localhost'),
ეს იქნება პირველი პარამეტრი, რომელსაც ჩვენ შევცვლით და მივუთითებთ იმ დომენს ან ip მისამართს სადაც გაშვებულია პროექტი, ჩემს შემთხვევაში ეს არის:
'url' => env('APP_URL', 'http://127.0.0.1:8000'),
შემდეგი გასაღები არის :
'timezone' => 'UTC',
სადაც ხდება დროის სარტყელის მითითება, ჩავანაცვლოთ ნაგულისმები მნიშვნელობა ჩვენთვის სასურველით
'timezone' => 'Asia/Tbilisi',
კიდევ ერთი გასაღები არის
'key' => env('APP_KEY'),
აქ ეთითება ფრეიმვორკის საიდუმლო გასაღები, შევნიშნოთ, რომ ამ შემთხვევაში "env" ფუნქციას არ გადაეცემა ნაგულისმები მნიშვნელობა არგუმენტად,
ეს იმიტომ რომ საიდუმლო გასაღების გენერირება ხდება ფრეიმვორკის ინსტალაციის დროს და მას აგენერირებს Composer-ი.
მასივის providers გასაღებში მოქცეულია ყველა ხელმისაწვდომი სერვის-პროვაიდერი, რომელთა ჩატვირთვაც ხდება ფრეიმვორკის ამუშავებისას. თუ რა არის სერვის-პროვაიდერ, ცოტა მოგვიანებით განვმარტავთ.
მასივის ბოლო გასაღები არის - aliases, მასში შეტანილია ფასადების ანუ სისტემური კლასების ფსევდონიმები. თუ რა არის ფასადი და როგორ ვიმუშაოთ მასთან, ცოტა მოგვიანებით განვმარტავთ.
'default' => env('DB_CONNECTION', 'mysql')
ანუ მონაცემთა ბაზის სამართავი ნაგულისმები სისტემა, შევამჩნიოთ რომ ამ გასაღების მნიშვნელობაც .env ფაილიდან მოდის env ფუნქციის
მეშვეობით, მაგრამ იქ მონაცემთა ბაზის სამართავი სისტემის დასახელება განსაზღვრული ჯერჯერობით არ გვაქვს, ამიტომ ან უნდა განვსაზღროთ იგი .env ფაილში
DB_CONNECTION=mysql
ანდა არ განვსაზღვროთ და სისტემა გამოიყენებს ნაგულისმებ მნიშვნელობას, რომელიც env ფუნქციას მეორე პარამეტრად აქვს გადაცემული.
$value = config('app.timezone');
// განვსაზღვროთ პარამეტრი თუ მისი მნიშვნელობა ვერ მოიძებნა
$value = config('app.timezone', 'Asia/Tbilisi');
კონფიგურაციული პარამეტრის მნიშვნელობის განსასაზღვრავად config დამხმარეს უნდა გადავცეთ პარამეტრისა და მისი მნიშვნელობის შემცველი
აცოციაციური მასივი :
config(['app.timezone' => 'America/Chicago']);
php artisan config:cache
ბრძანების გაშვების შემდეგ შეიქმნება ფაილი bootstrap/cache/config.php, რომელშიც აღწერილი იქნება ყველა კონფიგურაციული პარამეტრის
შემცველი ასოციაციური მასივი.
თუ config: cache ბრძანებას გავუშვებთ მუშაობის პროცესში ანუ მაშინ, როდესაც აპლიკაცია ჯერ დასრულებული არ იქნება, დარწმუნებულები უნდა ვიყოთ, რომ env() დამხმარე ფუნქციას ვიძახებთ, მხოლოდ და მხოლოდ კონფიგურაციულ ფაილებში და არსად სხვაგან, ვინაიდან ქეშირების შემდეგ env ფაილის ჩატვირთვა საერთოდ აღარ ხდება და შესაბამისად ვერც რაიმე პარამეტრის ამოღებას შევძლებთ მისგან (ქეშირებისას კონფიგურაციულ ფაილებში env() ფუნქციის დახმარებით განსაზღვრული პარამეტრები ავტომატურად შეინახებოდა bootstrap/cache/config.php ფაილში).
php artisan config:clear
ბრძანების გაშვების შემდეგ bootstrap/cache/config.php ფაილი წაიშლება.
'debug' => (bool) env('APP_DEBUG', false),
require __DIR__.'/../vendor/autoload.php';
ამის შემდეგ იქმნება აპლიკაციის გლობალური ობიექტი :
$app = require_once __DIR__.'/../bootstrap/app.php';
bootstrap/app.php :
...
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
...
return $app;
ამის შემდეგ საქმეში ერთვება მოთხოვნათა დამმუშვებელი ძირითადი კლასი Kernel, რომელიც აღწერილია app/Http/Kernel.php ფაილში.
ეს კლასი არის იგივე სახელწოდების მქონე Illuminate\Foundation\Http\Kernel კლასის მემკვიდრე
(სრული მისამართი : vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php). ამ კლასში აღწერილია $bootstrappers მასივი:
...
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, // გარემოს ცვლადთა ჩამტვირთველი
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class, // კონფიგურაციული პარამეტრების ჩამტვირთველი
\Illuminate\Foundation\Bootstrap\HandleExceptions::class, // გამონაკლისთა დამმუშავებელი
\Illuminate\Foundation\Bootstrap\RegisterFacades::class, // ფასადების რეგისტრაცია (ვისაუბრებთ ოდნავ მოგვიანებით)
\Illuminate\Foundation\Bootstrap\RegisterProviders::class, // პროვაიდერთა რეგისტრაცია (ვისაუბრებთ ოდნავ მოგვიანებით)
\Illuminate\Foundation\Bootstrap\BootProviders::class, // პროვაიდერთა ჩატვირთვა
];
...
სწორედ ამ კლასების ჩატვირთვა ხდება მანამ სანამ დამუშვდება მომხმარებლის მიერ გაკეთებული მოთხოვნა.
ამავე კლასის handle() მეთოდში ხდება მოთხოვნის შესაბამისი პასუხის გენერირება ანუ დაბრუნება.
დავუბრუნდეთ ისევ მემკვიდრე Kernel კლასს :)) მასში აღწერილია $middleware მასივი:
...
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
...
ამ მასივში აღწერილია ის შუამავლები, რომლებიც აუცილებლად უნდა გაიაროს მომხმარებლის მიერ გაკეთებულმა მოთხოვნამ (შუამავლების შესახებ ვისაუბრებთ ოდნავ მოგვიანებით).
ავტოჩატვირთვისა და ყველა საჭირო პროცესის ინიციალიზაციის შემდეგ მოთხოვნა გადაეცემა მარშრუტიზატორს და ფრეიმვორკი ღებულობს გადაწყვეტილებას თუ რომელმა მარშრუტმა უნდა დაამუშავოს ესა თუ ის მოთხოვნა. მარშრუტმა მოთხოვნა შეიძლება გადასცეს კონკრეტულ კონტროლერს ან უბრალოდ ფუნქციას, თუ მოთხოვნა კონტროლერს გადაეცემა, იგი ჩაატარებს შესაბამის სამუშაოებს (ინფორმაციის გადამოწმება, ვალიდაცია და ა.შ) და თავის მხრივ მიმართავს მოდელს, თუ აუცილებელია მოდელი მიმართავს მონაცემთა ბაზას , ბაზა დაუბრუნებს მას პასუხს, მოდელი ამ პასუხს დაუბრუნებს კონტროლერს, კონტროლერი კი მიღებულ პასუხს გადასცემს წარმოდგენის შაბლონს, წარმოდგენის შაბლონი უზრუნველჰყოფს შედეგის ბრაუზერში გამოტანას. ხოლო თუ მარშრუტიზატორი მოთხოვნას არ გადასცემს კონტროლერს, არამედ გადასცემს ფუნქციას, მაშინ ეს ფუნქცია დაამუშავებს მოთხოვნას და შედეგს გადასცემს პირდაპირ წარმოდგენას.ინგ: Injection - ინექცია; დანერგვა; შემოღება; ჩადება;
თუ კონკრეტული კლასის მუშაობისათვის საჭიროა, გამოყენებულ იქნას სხვა კლასის ფუნქციონალი, ეს იმას ნიშნავს, რომ პირველი კლასი დამოკიდებულია მეორე კლასზე. ბუნებრივია უნდა მოვახდინოთ დამხმარე კლასის საწყის კლასში ინტეგრირება (Injection) და მხოლოდ ამის შემდეგ შეგვეძლება დამხმარე კლასის ფუნქციონალის გამოყენება. სწორედ ამ დამხმარე კლასს ეწოდება დამოკიდებულება (Dependency).
...
use Illuminate\Http\Request;
...
class SomeController extends Controller
{
...
public function post(Request $request)
{
$request->validate([
// ...
]);
// ...
}
...
}
ამ მაგალითზე შეიძლება ითქვას, რომ SomeController კლასში მოვახდინეთ Request დამოკიდებულების
ინექცია.
Route::get('/', function(){
echo '<pre>';
print_r(app());
echo '</pre>';
die;
});
თუ ახლა აპლიკაციის მთავარ გვერდზე შევალთ, ვიხილავთ ამდაგვარ სურათს :
namespace App\Services;
class MathService
{
public function doAddition($numbers)
{
return array_sum($numbers);
}
}
ასევე შევქმნათ შესაბამისი მარშრუტი და კონტროლერი :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\MathController;
Route::get('/math', [MathController::class, 'index']);
MathController :
namespace App\Http\Controllers;
use App\Services\Mathservice;
class MathController extends Controller
{
public function index()
{
$serv = new Mathservice();
dd($serv->doAddition([4,6])); // 10
}
}
გადავაკეთოთ კონტროლერი ამდაგვარად :
namespace App\Http\Controllers;
use App\Services\Mathservice;
class MathController extends Controller
{
public function index(Mathservice $serv)
{
dd($serv->doAddition([4,6])); // 10
}
}
შედეგი აქაც იგივე იქნება, ანუ Mathservice სერვისის ამ სახით ინექციაც გასაგებია სისტემისათვის.
ახლა ამოცანა და შესაბამისად სერვისიც, გადავაკეთოთ ასე : სერვისმა დაგვიბრუნოს არა მასივის ელემენტების ჯამი, არამედ ამ ჯამს დამატებული კიდევ ერთი რიცხვი, რომელიც აღწერილი იქნება სერვისის კერძო თვისებაში - $add_param :
namespace App\Services;
class MathService
{
private $add_param;
public function __construct($add_param)
{
$this->add_param = $add_param;
}
public function doAddition($numbers)
{
return array_sum($numbers) + $this->add_param;
}
}
ასეთ შემთხვევაში ვიხილავთ შემდეგ შეტყობინებას :
Unresolvable dependency resolving [Parameter #0 [ <required> $add_param ]] in class App\Services\MathService
ეს იმას ნიშნავს, რომ სისტემამ ვერ იპოვა ის დამოკიდებულება, რომელიც მოვთხოვეთ. სწორედ ამ პრობლემის მოგვარებაში დაგვეხმარება სერვისისების კონტეინერი
და სერვისის პროვაიდერი.
ნებისმიერი პროვაიდერი არის Illuminate\Support\ServiceProvider კლასის მემკვიდრე და შეიცავს ორ მეთოდს : register და boot. როდესაც აპლიკაცია იტვირთება, სისტემა აკითხავს ყველა არსებული პროვაიდერის register მეთოდს და ასრულებს თითოეულ მათგანში აღწერილ ინსტრუქციებს. პროვაიდერის შექმნა შესაძლებელია შემდეგი ბრძანების მეშვეობით :
php artisan make:provider MathServiceProvider
namespace App\Providers;
use App\Services\Mathservice;
use Illuminate\Support\ServiceProvider;
class MathServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->bind(Mathservice::class, function ($app) {
// add_param : პარამეტრი, რომელიც უნდა დაემატოს მასივის ელემენტთა ჯამს
return new Mathservice(add_param : 25);
});
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
}
}
ახლა დავარეგისტრიროთ პროვაიდერი config/app.php ფაილში აღწერილ მასივში:
...
'providers' => [
...
App\Providers\MathServiceProvider::class,
...
],
...
ახლა უკვე ყველაფერი რიგზე იქნება. ანუ ჩვენ Mathservice სერვისი დავარეგისტრირეთ სერვისების კონტეინერში სერვისის პროვაიდერის
დახმარებით:
namespace App\Http\Controllers;
use App\Services\Mathservice;
class MathController extends Controller
{
public function index(Mathservice $serv)
{
dd($serv->doAddition([4,6])); // 4 + 6 = 10 + 25 = 35
}
}
...
public function register()
{
$this->app->singleton(Someclass::class, function ($app) {
//
});
}
...
namespace App\Providers;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class ComposerServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
View::composer('view', function () {
//
});
}
}
წარმოდგენის ფაილებზე უფრო დაწვრილებით ვისაუბრებთ მოგვიანებით.
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;
Route::get('/cache', function () {
return Cache::get('key');
});
namespace App\Services;
class MathService
{
public static function doAddition($numbers)
{
return array_sum($numbers);
}
}
შევქმნათ ფაილი app/Helpers/Facades/Mathfacade.php. პირველი რაც ფასადის შექმნისას უნდა გავაკეთოთ, არის ის, რომ ხელახლა უნდა აღვწეროთ
აბსტრაქტული მშობელი კლასის - Facade-ს getFacadeAccessor მეთოდი :
namespace App\Helpers\Facades;
use Illuminate\Support\Facades\Facade;
class Mathfacade extends Facade
{
protected static function getFacadeAccessor()
{
return 'mathfacade';
}
}
ამ მეთოდის დანიშნულება არის ის, რომ დააბრუნოს სერვისების კონტეინერში არსებული, კონკრეტული ფუნქციონალის შესაბამისი
სიტყვაგასაღები ანუ მეტსახელი. ჩვენს შემთხვევაში ეს მეტსახელი არის -
mathfacade. ახლა გადავინაცვლოთ სერვისპროვაიდერში :
namespace App\Providers;
use App\Services\Mathservice;
use Illuminate\Support\ServiceProvider;
class MathServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->bind('mathfacade', function ($app) {
return new Mathservice();
});
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
}
}
public __call ( string $name , array $arguments ) : mixed
public static __callStatic ( string $name , array $arguments ) : mixed
მაგალითად :
class MethodTest
{
public function __call($name, $arguments)
{
echo "მეთოდი '$name' " . implode(', ', $arguments). "\n";
}
public static function __callStatic($name, $arguments)
{
echo "მეთოდი '$name' " . implode(', ', $arguments). "\n";
}
}
$obj = new MethodTest;
$obj->runTest('ობიექტის კონტექსტში');
MethodTest::runTest('სტატიკურ კონტექსტში');
ამ კოდის შედეგი იქნება :
მეთოდი 'runTest' ობიექტის კონტექსტში
მეთოდი 'runTest' სტატიკურ კონტექსტში
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance)
{
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
ამ მეთოდის გამოძახება ხდება მაშინ, როდესაც მივმართავთ ფასადის არარსებულ მეთოდს. იგი ახდენს ფასადის მეთოდის გადამისამართებას
სერვისების კონტეინერში არსებული, სასურველი კლასის ობიექტზე და რეალურად ჩვენ უკვე ამ ობიექტს და მის მეთოდს მივმართავთ.
__callStatic მეთოდში თავიდანვე ხდება getFacadeRoot() სტატიკური მეთოდის გამოძახება, დავაკვირდეთ ამ მეთოდს :
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
ეს მეთოდი დავის მხრივ იძახებს resolveFacadeInstance მეთოდს, რომელსაც არგუმენტად გადაეცემა, ჩვენს მიერ დასაწყისშივე აღწერილი
მეთოდი getFacadeAccessor. resolveFacadeInstance მეთოდის კოდი ასეთია :
protected static function resolveFacadeInstance($name)
{
if (is_object($name))
{
return $name;
}
if (isset(static::$resolvedInstance[$name]))
{
return static::$resolvedInstance[$name];
}
if (static::$app)
{
return static::$resolvedInstance[$name] = static::$app[$name];
}
}
განსაკუთრებული ყურადღება მივაქციოთ ამ ჩანაწეერს : static::$app[$name], სწორედ $app
სტატიკური თვისების უკან მოიაზრება სერვისების კონტეინერი. თავად ჩანაწერი კი აბრუნებს სერვისების კონტეინერში, ჩვენს მიერ განსაზღვრული სიტყვაგასაღების
(mathfacade) შესაბამის კლასს, ეს კლასი კი სერვისის პროვაიდერში გვაქვს აღწერილი :
namespace App\Providers;
use App\Services\Mathservice;
use Illuminate\Support\ServiceProvider;
class MathServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind('mathfacade', function ($app) {
return new Mathservice();
});
}
...
}
შესაბამისად წორედ Mathservice() კლასის ობიექტთან მოხდება გადამისამართება, როდესაც ფასადის არარსებულ მეთოდს
გამოვიძახებთ.
იმისათვის რათა ჩვენმა ფასადმა იმუშავოს, უნდა დავარეგისტრიროთ აღნიშნული ფასადის შესაბამისი ფსევდონიმი, ეს უნდა გავაკეთოთ config/app.php ფაილში :
...
'providers' => [
...
App\Providers\MathServiceProvider::class,
],
'aliases' => [
...
'Mathfacade' => App\Helpers\Facades\Mathfacade::class,
],
...
ახლა კი შეგვიძლია მივმართოთ ჩვენს ფასადს, ეს გავაკეთოთ MathController-ში:
namespace App\Http\Controllers;
use Mathfacade;
class MathController extends Controller
{
public function index()
{
$res = Mathfacade::doAddition([2,5]);
dd($res); // 7
}
}
https://vnadiradze.ge/info/laravel/index.html
URI შედგება ორი ნაწილისაგან : URL და URN.
http://vnadiradze.ge
/info/laravel/index.html
'Route' => Illuminate\Support\Facades\Route::class,
ფასადის შემდეგ უნდა მივუთითოთ HTTP მოთხოვნის ტიპი, მოთხოვნის ტიპს კი პირველ პარამეტრად უნდა გადავცეთ შაბლონი, ანუ URI-ს ის ნაწილი, რომლისთვისაც ვქმნით
ამ მარშრუტს. ჯერჯერობით ვართ ამ ეტაპზე :
Route::get('/page')
ანუ მოცემული მარშრუტი ამუშავდება მაშინ თუ მომხმარებელი შევა შემდეგ მისამართზე :
example.com/page
ყველაზე მარტივ შემთხვევაში მოთხოვნის ტიპს მეორე პარამეტრად შეიძლება გადაეცეს ფუნქცია დამმუშავებელი :
Route::get('/page', function(){
});
ამ ფუნქციის ტანში შესაძლებელია ნებისმიერი კოდის ჩაწერა. მაგალითად ეკრანზე გამოვიტანოთ ფრეიმვორკის კონფიგურაციის რომელიმე პარამეტრის მნიშვნელობა.
ვთქვათ app კონფიგურაციული ჯგუფის locale პარამეტრის მნიშვნელობა:
Route::get('/page', function(){
echo config('app.locale');
});
იგივეს გაკეთება შეგვეძლო Config ფასადის get მეთოდით:
Route::get('/page', function(){
echo Config::get('app.locale');
});
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<form action="/comments" method="post">
<input type="text" name="fname" placeholder="სახელი">
<input type="text" name="lname" placeholder="გვარი">
<input type="submit" name="send" value="გაგზავნა">
</form>
</body>
</html>
ახლა დავწეროთ შესაბამისი მარშრუტი:
Route::post('/comments' , function(){
print_r($_POST);
});
თუ ახლა შევალთ შემდეგ მისამართზე :
http://127.0.0.1:8000/comments
ბრაუზერში ვიხილავთ შეტყობინებას :
The GET method is not supported for this route. Supported methods: POST.
ეს იმიტომ, რომ ჩვემ მარშრუტი დავწერეთ post მეთოდისათვის და მივაკითხეთ get მეთოდით. თუ შევალთ ამ მისამართზე :
http://127.0.0.1:8000/form.html
მაშინ ვიხილავთ შესაბამის ფორმას, რომელიც ინფორმაციას post მეთოდით გააგზავნის http://127.0.0.1:8000/comments გვერდზე.
419 | PAGE EXPIRED
ეს მოხდა CSRF (Cross-Site Request Forgery) ვერიფიკაციის გამო, ანუ სისტემამ ჩათვალა, რომ გვერდის მოთხოვნა მოხდა არასწორი
სახით. CSRF ვერიფიკაციის შესახებ უახლოეს ხანებში ვისაუბრებთ, ამჟამად კი ეს პრობლემა ასე მოვაგვაროთ :
app/Http/Middleware/VerifyCsrfToken.php საქაღალდეში ჩავამატოთ შემდეგი გამონაკლისი :
protected $except = [
'/comments'
];
ახლა http://127.0.0.1:8000/form.html ფორმის გაგზავნის შემდეგ გადავალთ http://127.0.0.1:8000/comments გვერდზე სადაც
ვიხილავთ გლობალურ ცვლად $_POST-ში მოქცეულ იმ ინფორმაციას, რომელიც ფორმაში ავკრიფეთ.
Route::match(['get','post'] , '/comments' , function(){
print_r($_POST);
});
ამ შემთხვევაში უკვე შეგვეძლება ფორმის გაგზავნის გარეშე http://127.0.0.1:8000/comments გვერდზე შესვლა.
Route::any('/comments' , function(){
print_r($_POST);
});
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UserController;
Route::get('/user', [UserController::class, 'index']);
Route::redirect('/here', '/there');
Route::view('/welcome', 'welcome');
ამის შემდეგ http://127.0.0.1:8000/welcome მისამართზე შესვლისას ჩაიტვირთება resources/views/welcome.blade.php ფაილი.
Route::get('/user/{id}', function ($id) {
return 'User '.$id;
});
ანუ პარამეტრები ექცევა ფიგურულ ფრჩხილებში. შესაძლებელია ერთდროულად რამდენიმე პარამეტრის გადაცემაც :
Route::get('/page/{category}/{id}' , function(){
});
თუ ახლა შევალთ შემდეგ მისამართზე :
http://127.0.0.1:8000/page
ვიხილავთ შეტყობინებას, რომ გვერდი ვერ მოიძებნა, მაგრამ თუ შევალთ ამ მისამართზე :
http://127.0.0.1:8000/page/sport/10
ყველაფერი რიგზე იქნება.
გადაცემულ პარამეტრთან წვდომისათვის ეს პარამეტრი არგუმენტად უნდა გადავცეთ მარშრუტში აღწერილი ფუნქცია დამმუშავებელს :
Route::get('/page/{id}' , function($id){
echo $id;
});
როგორც ზემოთ აღვნიშნეთ, შესაძლებელია მარშრუტს მიეთითოს რამდენიმე ცვლადი პარამეტრი, აქ უნდა გავითვალისწინოთ ერთი ფაქტი: პარამეტრები ფუნქცია დამმუშავებელსაც
იმავე თანმიმდევრობით უნდა გადავცეთ რა თანმიმდევრობითაც მარშრუტში აღვწერთ მათ. ფუნქცია ამ პარამეტრებს სწორედ თანმიმდევრობიდან გამომდინარე აღიქვამს და
არა ცვლადთა დასახელებებიდან.
Route::get('/page/{category}/{id}',function($c,$i){
echo "category - " . $c;
echo "id - " . $i;
});
თუ ახლა შევალთ შემდეგ მისამართზე :
http://127.0.0.1:8000/page/cars/10
ბრაუზერში ვიხილავთ შემდეგ ტექსტს :
category - cars
id - 12
Route::get('/user_null/{name?}', function ($name = null) {
return $name; // ცარიელი
});
Route::get('/user_name/{name?}', function ($name = 'ვასო') {
return $name; // ვასო
});
ამ შემთხვევაში უშეცდომოდ შევალთ
http://127.0.0.1:8000/user_null
გვერდზეც და
http://127.0.0.1:8000/user_name
გვერდზეც.
Route::get('/page/{id}',function($id){
echo $id;
})->where('id','[0-9]+');
ანუ სისტემას ვეუბნებით, რომ id პარამეტრი უნდა იყოს ციფრი და იგი შეიძლება მეორდებოდეს მრავალჯერ (ამას აღნიშნავს რეგ. გამოსახულებაში არსებული "+" ნიშანი),
რადგან id შეიძლება იყოს 5-იც და 345343-იც. ასეთ შემთხვევაში შემდეგ მისამართზე შესვლა :
http://127.0.0.1:8000/page/cars
გამოიტანს შეცდომას.
დავუშვათ რამდენიმე პარამეტრთან ერთად ვმუშაობთ და საჭიროა ყველას გაფილტვრა where მეთოდით: პირველი პარამეტრი უნდა შეიცავდეს მხოლოდ ლათინური ანბანის დიდ ან პატარა ასოებს, მეორე პარამეტრი კი მხოლოდ ციფრებს. ასეთ შემთხვევაში მეთოდის გამოყენების სინტაქსი შემდეგნაირია :
Route::get('/page/{cat}/{id}',function($cat,$id){
echo $id;
})->where(['cat'=>'[A-Za-z]+' , 'id'=>'[0-9]+']);
public function boot()
{
//
parent::boot();
}
ჩავამატოთ მასში სასურველი ფილტრი
public function boot()
{
Route::pattern('id', '[0-9]+');
parent::boot();
}
ამ ჩანაწერის ჩამატების შემდეგ აღარ დაგვჭირდება ყოველი მარშრუტის აღწერისას პარამეტრის თავიდან გაფილტვრა. მარშრუტს წავუშალოთ id პარამეტრის გაფილტვრის ნაწილი
Route::get('/page/{id}',function($cat,$id){
echo $id;
});
ყველაფერი იმუშავებს ისევ კორექტულად.
რამდენიმე პარამეტრის ერთდროულად, გლობალურად გაფილტვრისთვის კი boot მეთოდში უნდა ჩავამატოთ შემდეგი ჩანაწერი
Route::patterns(['id'=>'[0-9]+' , 'cat'=>'[A-Za-z]+']);
მეთოდი მიიღებს ასეთ სახეს :
public function boot()
{
Route::patterns(['id'=>'[0-9]+' , 'cat'=>'[A-Za-z]+']);
parent::boot();
}
მარშრუტიდან კი საერთოდ წავშალოთ გაფილტვრის სინტაქსი :
Route::get('/page/{cat}/{id}',function($cat,$id){
echo $id;
});
http://127.0.0.1:8000/application/administrator/index.php
http://127.0.0.1:8000/application/administrator/home.php
http://127.0.0.1:8000/application/administrator/create.php
http://127.0.0.1:8000/application/administrator/edit.php
http://127.0.0.1:8000/application/administrator/delete.php
...
ბუნებრივია თითოეული მათგანის მარშრუტის განსაზღვრისას, მარშრუტის შაბლონში უნდა გავიმეოროთ ეს საერთო ფრაზა, პრეფიქსი
application/administrator, ამის თავიდან ასაცილებლად უნდა გამოვიყენოთ მარშრუტთა ჯგუფი,
მასთან მუშაობა შესაძლებელია Route ფასადის group მეთოდის დახმარებით :
Route::group(['prefix'=>'application/administrator'],function(){
Route::get('/index',function(){
echo '/index';
});
Route::get('/home',function(){
echo '/home';
});
Route::get('/create',function(){
echo '/create';
});
Route::get('/edit',function(){
echo '/edit';
});
Route::get('/delete',function(){
echo '/delete';
});
});
Route::get('/user/profile', function () {
//
})->name('profile');
სახელის დარქმევა შესაძლებელია ასეც :
Route::get('/user/profile', [UserProfileController::class, 'show'])->name('profile');
Route::get('/test',function(){
echo $url = route('profile'); // http://127.0.0.1:8000/user/profile
return redirect()->route('profile'); // გადამისამართება
});
თუ ასხელდებულ მარშრუტს გადაეცემა პარამეტრები, შეგვიძლია ეს პარამეტრები განვსაზღვროთ route დამხმარე ფუნქციის მეორე პარამეტრში :
Route::get('/user/{id}/profile', function ($id) {
//
})->name('profile');
$url = route('profile', ['id' => 1]);
თუ ამ მასივს გადავცემთ დამატებით პარამეტრებსაც, მაშინ გასაღები/მნიშვნელობა წყვილები ავტომატურად ჩაჯდება URL-ში get ტიპის პარამეტრებად :
Route::get('/user/{id}/profile', function ($id) {
//
})->name('profile');
$url = route('profile', ['id' => 1, 'photos' => 'yes']);
// /user/1/profile?photos=yes
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->route()->named('profile'))
{
//
}
return $next($request);
}
Route::middleware(['first', 'second'])->group(function () {
Route::get('/', function () {
// გაივლის first & second შუამავლებს...
});
Route::get('/user/profile', function () {
// გაივლის first & second შუამავლებს...
});
});
Route::fallback(function () {
echo "გვერდი ვერ მოიძებნა";
});
use Illuminate\Support\Facades\Route;
$route = Route::current(); // Illuminate\Routing\Route
$name = Route::currentRouteName(); // მარშრუტის დასახელება
$action = Route::currentRouteAction(); // შესაბამისი კონტროლერის ის მეთოდი, რომელიც ამუშავებს მოთხოვნას
php artisan route:list
როდესაც პროექტი დასრულდება და რეალურ გარემოში გაეშვება, სასურველია, რომ მოვახდინოთ მარშრუტთა ქეშირება.
ეს საგრძნობლად შეამცირებს აპლიკაციის შატვირთვისას ყველა საჭირო მარშრუტის რეგისტრაციის დროს. ქეშირება ხდება
Artisan-ის route:cache ბრძანებით :
php artisan route:cache
ქეშირების შემდეგ ყოველი მოთხოვნის გაკეთებისას ჩაიტვირთება ქეშირებულ მარშრუტთა ფაილი, რომელიც შეიქმნებოდა - bootstrap/cache საქაღალდეში.
php artisan route:clear
php artisan make:middleware MiddlewareName
შევქმნათ შუამავალი კლასი სახელად CheckIP
php artisan make:middleware CheckIP
მისი ნახვა შესაძლებელია app/Http/Middleware საქაღალდეში.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckIP
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
return $next($request);
}
}
კლასის handle მეთოდს (ინგ: Handle - გარჩევა; განხილვა; რეგულირება; კონტროლის განხორციელება; განკარგვა) გადაეცემა ორი პარამეტრი, პირველი ეს არის $request
მოთხოვნა - შუამავალი კლასი მუშაობს მხოლოდ და მხოლოდ მოთხოვნასთან ერთად. შემდეგი პარამეტრი კი არის ფუნქცია $next, რომელიც მართვას გადასცემს შუამავალ
კლასთა ჯაჭვში არსებულ შემდეგ შუამავალ კლასს (თუ ზედა სურათს დავაკვირდებით, შევამჩნევთ, რომ შეიძლება არსებობდეს რამდენიმე შუამავალი ერთდროულად, პასუხი
არ დაბრუნდება მანამ სანამ ყველა მათგანი არ გააკეთებს თავის საქმეს). ყველა შუამავლის გავლის შემდეგ მოთხოვნა უკვე მიემართება აპლიკაციის ბირთვისაკენ შემდგომი დამუშავების
მიზნით.
აღვწეროთ რაიმე მარტივი ფუნქცია შუამავალ კლასში, მაგალითად გადავამოწმოთ ემთხვევა თუ არა მომხმარებლის IP მისამართი კონკრეტულ მნიშვნელობას, თუ ემთხვევა მაშინ გადავამისამართოთ მთავარ გვერდზე. ჩავამატოთ შესაბამისი ლოგიკა შუამავალ კლასში :
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckIP
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if($request->ip() == 'XXX.XXX.XXX.XXX')
{
return redirect()->route('index');
}
return $next($request);
}
}
ამ შუამავლის მიმაგრება შესაძლებელია როგორც ყველა მარშრუტზე ასევე რომელიმე კონკრეტულ მათგანზე. მივამაგროთ იგი კონკრეტულ მარშრუტს. პირველ რიგში
უნდა გავხსნათ HTTP მოთხოვნების დამუშავების ბირთვი ფაილი - app/Http/Kernel.php, რომელშიც აღწერილია კლასი Kernel, ამ კლასში არის
დახურული თვისება $routeMiddleware, ამ თვისებაში ასოციაციური მასივის სახით აღწერილია ის შუამავლები, რომელთა გამოყენებაც შეგვიძლია მარშრუტებთან მუშაობისას,
მასივის გასაღებები წარმოადგენენ შუამავალი კლასების ფსევდონიმებს რათა მარტივად შეგვეძლოს მათთან მიმართვა. ჩავამატოთ ჩვენი შექმნილი შუამავალი კლასი :
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'checkIP' => \App\Http\Middleware\CheckIP::class
];
Route::get('/test', function () {
//
})->middleware('checkIP');
მარშრუტზე რამდენიმე შუალედური კლასის მიმაგრება კი ხდება ასე :
Route::get('/', function () {
//
})->middleware(['first', 'second']);
შუამავალი კლასის მიმაგრება შესაძლებელია მარშრუტის დამმუშავებელ კონტროლერშიც, მეთოდი კონსტრუქტორის მეშვეობით :
public function __construct()
{
$this->middleware('checkIP');
}
როდესაც შუამავლებს ვამაგრებთ მარშრუტთა ჯგუფს, შესაძლებელია დაგვჭირდეს ისე, რომ ეს შუამავალი არ შეეხოს რომელიმე მათგანს ამ ჯგუფიდან.
ამაში დაგვეხმარება withoutMiddleware მეთოდი :
use App\Http\Middleware\CheckIP;
Route::middleware([CheckIP::class])->group(function () {
Route::get('/', function () {
//
});
Route::get('/profile', function () {
//
})->withoutMiddleware([CheckIP::class]);
});
როგორც ვხედავთ routes/web.php ფაილში კლასის სრული დასახელების გამოყენებითაცაა შესაძლებელი შუამავლების მიმაგრება მარშრუტებზე.
Laravel-ის ინსტალაციის შემდეგ ავტომატურად გენერირდება მარშრუტთა web და api ჯგუფები, რომლებიც მოიცავენ web და api მარშრუტებთან ყველაზე ხშირად გამოყენებად შუამავლებს.
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
აღსანიშნავია, რომ ეს ჯგუფები ავტომატურად ემაგრება ჩვენს აპლიკაციას App\Providers\RouteServiceProvider სერვის-პროვაიდერის მიერ :
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
}
კონტროლერები ინახება app/Http/Controllers საქაღალდეში. ნაგულისხმეობის პრინციპით ამ საქაღალდეში უკვე შექმნილია ერთი საბაზისო კონტროლერი Controller.php სწორედ მისი მემკვიდრეები უნდა იყვნენ ის კონტროლერები, რომლებსაც მომავალში შევქმნით.
class UserController extends Controller
{
}
ახლა განსაზღვროთ კონტროლერის namespace ანუ სახელსივრცე :
namespace App\Http\Controllers
class UserController extends Controller
{
}
თუ ვქმნით კლასს, რომელიც არის სხვა კლასის მემკვიდრე, მაშინ მშობელ კლასთანაც უნდა გვქონდეს წვდომა :
namespace App\Http\Controllers
use App\Http\Controllers\Controller;
class UserController extends Controller
{
}
ახლა შევქმნათ მარშრუტი, რომელსაც დაამუშავებს შექმნილი კონტროლერი, routes/web.php :
use App\Http\Controllers\UserController;
Route::get('/user/{id}', [UserController::class, 'show']);
შევქმნათ კონტროლერის შესაბამისი მეთოდი show :
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
class UserController extends Controller
{
public function show($id)
{
echo $id;
}
}
შევიდეთ http://127.0.0.1:8000/user/1 მისამართზე.
php artisan make:controller ControllerName
შევქმნათ კონტროლერი TestController.php
php artisan make:controller TestController
თუ შევალთ კონტროლერების საქაღალდეში, დაგვხვდება ახალი კონტროლერი.
Route::get('profile', [UserController::class, 'show'])->middleware('auth');
ასევე შესაძლებელია შუამავლის განსაზღვრა კონტროლერის მეთოდ კონსტრუქტორშიც :
class UserController extends Controller
{
/**
* Instantiate a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('log')->only('index');
$this->middleware('subscribed')->except('store');
}
}
php artisan make:controller PhotoController --resource
ამ ბრძანების შედეგად შეიქმნება app/Http/Controllers/PhotoController.php კონტროლერი, რომელშიც უკვე აღწერილი იქნება ყველა ზემოთ
ნახსენები საჭირო მეთოდი.
ახლა აღვწეროთ რესურსის ტიპის მარშრუტი Route ფასადის resource მეთოდის დახმარებით :
Route::resource('photos', PhotoController::class);
ეს ერთადერთი ჩანაწერი ახდენს ოთხივე ოპერაციისათვის (CRUD) საჭირო ყველა მარშრუტის დეკლარირებას. საერთო ჯამში კი მიიღება ამდაგვარი სურათი :
მოთხოვნის ტიპი | URI | კონტროლერის ფუნქცია (მეთოდი) | მარშრუტის დასახელება |
---|---|---|---|
GET | /photos | index | photos.index |
GET | /photos/create | create | photos.create |
POST | /photos | store | photos.store |
GET | /photos/{photo} | show | photos.show |
GET | /photos/{photo}/edit | edit | photos.edit |
PUT/PATCH | /photos/{photo} | update | photos.update |
DELETE | /photos/{photo} | destroy | photos.destroy |
ინგ: Injection - ინექცია; დანერგვა; შემოღება; ჩადება;
თუ კონკრეტული კლასის მუშაობისათვის საჭიროა, გამოყენებულ იქნას სხვა კლასის ფუნქციონალი, ეს იმას ნიშნავს, რომ პირველი კლასი დამოკიდებულია მეორე კლასზე. ბუნებრივია უნდა მოვახდინოთ დამხმარე კლასის საწყის კლასში ინტეგრირება (Injection) და მხოლოდ ამის შემდეგ შეგვეძლება დამხმარე კლასის ფუნქციონალის გამოყენება. სწორედ ამ დამხმარე კლასს ეწოდება დამოკიდებულება (Dependency).
...
use Illuminate\Http\Request;
...
class SomeController extends Controller
{
...
public function post(Request $request)
{
$request->validate([
// ...
]);
// ...
}
...
}
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
class UserController extends Controller
{
protected $users;
public function __construct(UserRepository $users)
{
$this->users = $users;
}
}
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request)
{
$name = $request->name;
//
}
}
თუ მოხდა ისე, რომ კონტროლერს გადმოეცემა მარშრუტის პარამეტრიც,
მაშინ ეს პარამეტრი უნდა აღვწეროთ ინექციის შემდეგ.
მაგალითად თუ გვაქვს ასეთი მარშრუტი :
use App\Http\Controllers\UserController;
Route::put('/user/{id}', [UserController::class, 'update']);
კონტროლერის მეთოდს ეს პარამეტრი ინექციასთან ერთად გადაეცემა ამდაგვარად :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function update(Request $request, $id)
{
//
}
}
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PostsController;
Route::get('/posts', [PostsController::class, 'index']); // პოსტების ჩამონათვალი
Route::get('/posts/{post}', [PostsController::class, 'show']); // კონკრეტული პოსტი
Route::post('/posts', [PostsController::class, 'store']); // ახალი პოსტის დამატება
ლარაველის მე-9-ე ვერსიაში შესაძლებელია მარშრუტთა დაჯგუფება კონტროლერის
მიხედვით, ამისათვის გამოიყენება Route ფასადის controller მეთოდი group
მეთოდთან ერთად :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PostsController;
Route::controller(PostsController::class)->group(function(){
Route::get('/posts', 'index'); // პოსტების ჩამონათვალი
Route::get('/posts/{post}', 'show'); // კონკრეტული პოსტი
Route::post('/posts', 'store'); // ახალი პოსტის დამატება
});
როგორც ვხედავთ, მარშრუტებისათვის, სათითაოდ ცალ-ცალკე კონტროლერის განსაზღვრა აღარ გვჭირდება და უბრალოდ კონტროლერის
მეთოდების დასახელებებს ვუთითებთ.
იმისათვის რათა მივიღოთ მიმდინარე HTTP მოთხოვნის ობიექტი, Illuminate\Http\Request კლასი, დამოკიდებულებათა ინექციის საშუალებით უნდა ჩავსვათ საჭირო კონტროლერში ან მარშრუტის ფუნქცია-დამმუშავებელში:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request)
{
$name = $request->input('name');
//
}
}
მარშრუტის შემთხვევაში :
use Illuminate\Http\Request;
Route::get('/', function (Request $request) {
//
});
$uri = $request->path(); // foo/bar
if ($request->is('foo/*'))
{
//
}
if ($request->routeIs('index'))
{
//
}
// http://127.0.0.1:8000/foo/bar?want_drink=true
$request->url() // http://127.0.0.1:8000/foo/bar
$request->fullUrl() // http://127.0.0.1:8000/foo/bar?want_drink=true
თუ გვინდა, რომ მიმდინარე მისამართს მივაწეროთ დამატებითი GET პარამერები, მაშინ უნდა გამოვიყენოთ fullUrlWithQuery მეთოდი :
// http://127.0.0.1:8000/foo/bar?want_drink=true
$request->url() // http://127.0.0.1:8000/foo/bar
$request->fullUrl() // http://127.0.0.1:8000/foo/bar?want_drink=true
$request->fullUrlWithQuery(['drink' => 'vodka'])) // http://127.0.0.1:8000/foo/bar?want_drink=true&drink=vodka
$method = $request->method();
if ($request->isMethod('get'))
{
//
}
$ipAddress = $request->ip();
<input type="text" name="name">
<input type="password" name="password">
public function login(Request $request)
{
print_r($request->all());
}
შედეგად ვიხილავთ დაახლოებით შემდეგი სახის მასივს :
Array
(
[name] => vaso
[password] => pass123
)
// http://127.0.0.1:8000/foo/bar?want_drink=true
$want = $request->input('want_drink')); // true
$drink = $request->input('drink', 'vodka')); // vodka
უნდა აღინიშნოს, რომ input() მეთოდი მუშაობს ნებისმიერი ტიპის HTTP მოთხოვნებთან.
$name = $request->query('name', 'Helen');
თუ მეთოდს საერთოდ არ გადავცემთ პარამეტრებს, მაშინ იგი დაგვიბრუნებს GET ტიპის ყველა პარამეტრს.
$archived = $request->boolean('archived');
$name = $request->name;
დინამიური მეთოდის გამოყენებისას ფრეიმვორკი პირველ რიგში გადაამოწმებს მოთხოვნაში ჩადებულ ველებს, თუ აქ ვერ მოიძებნა შესაბამისი ველი, მაშინ გადაამოწმებს
მარშრუტის პარამეტრებს.
$input = $request->only(['username', 'password']);
$input = $request->only('username', 'password');
მეთოდს პარამეტრად უნდა გადაეცეს შესაბამისი ველის დასახელება.
$input = $request->except(['credit_card']);
$input = $request->except('credit_card');
მეთოდს პარამეტრად უნდა გადაეცეს შესაბამისი ველის დასახელება (name ატრიბუტის მნიშვნელობა)
if ($request->has('name'))
{
//
}
if ($request->filled('name'))
{
//
}
$request->flash();
შენახული ინფორმაცია შეიძლება გამოიყურებოდეს ასე :
Array
(
[_token] => xtLofMZlLR1b4oEZeT9YPr8SZiOzqfLrGof9huOj
[_previous] => Array
(
[url] => http://127.0.0.1:8000/contact
)
[_flash] => Array
(
[old] => Array
(
)
[new] => Array
(
[0] => _old_input
)
)
[_old_input] => Array
(
[name] => vaso
[password] => pass123
)
)
_token არის საიტის უსაფრთხოების გასაღები და მის შესახებ მოგვიანებით ვისაუბრებთ. რაც შეეხება _old_input უჯრას - იგი შეიცავს ბოლო
მოთხოვნაში შესული ინფორმაციის შემცველ მასივს. ამ ინფორმაციასთან წვდომისათვის გამოიყენება სპეციალური ფუნქცია old(), ფორმის
წარმოდგენის ფაილი გადავაკეთოთ ასე :
<input type="name" name="name" value="{{ old('name') }}">
<input type="password" name="password" value="{{ old('password') }}">
ამის შემდეგ თუ ფორმას გავაგზავნით ვნახავთ, რომ აკრეფილი ინფორმაცია არ დაიკარგება და ველები ავტომატურად შეივსება.
$request->flashOnly(['username', 'email']);
$request->flashExcept('password');
კონკრეტულ გვერდზე რაიმე ინფორმაციის გამოტანის, ანუ ერთგვარი პასუხის დაბრუნების უმარტივესი გზა არის კონტროლერში ან მარშრუტის დამმუშავებელში ტექსტის დაბრუნება :
public function index()
{
return 'Hello World';
}
Route::get('/', function () {
return 'Hello World';
});
ასევე შესაძლებელია მასივის დაბრუნებაც, ფრეიმვორკი ავტომატურად გადააფორმატებს მას JSON ფორმატში :
Route::get('/', function () {
return [1, 2, 3];
});
public function __construct($content = '', $status = 200, array $headers = [])
{
$this->headers = new ResponseHeaderBag($headers);
$this->setContent($content);
$this->setStatusCode($status);
$this->setProtocolVersion('1.0');
}
ანუ შეგვიძლია განვსაზღვროთ პასუხის აღწერილობა, HTTP სტატუსი და სათაურები :
Route::get('/home', function () {
return response('Hello World', 200)
->header('Content-Type', 'text/plain');
});
return response('Hello World')->cookie('name', 'value', $minutes);
return response('Hello World')->withoutCookie('name');
/**
* იმ cookie-ბის დასახელებები, რომელთა შიფრაციაც არ მოხდება
*
* @var array
*/
protected $except = [
'cookie_name',
];
Route::get('/dashboard', function () {
return redirect('home/dashboard');
});
ზოგჯერ საჭიროა, რომ მომხმარებელი გადავამისამართოთ წინა გვერდზე (მაგალითად გაგზავნა ფორმა არავალიდური ინფორმაციებით),
ანუ დავაბრუნოთ უკან, ამისათვის გამოიყენება დამხმარე ფუნქცია back. ფუნქცია იყენებს სესიებს, ამიტომ დარწმუნებულები უნდა ვიყოთ,
რომ მარშრუტი, რომლის დამმუშავებელშიც back ფუნქციას ვიძახებთ, მოქცეულია web შუამავალში (როგორც ვიცით სწორედ ეს შუამავალი ახდენს,
სესიების მიმაგრებას მარშრუტებთან) :
Route::post('/user/profile', function () {
// მოთხოვნის ვალიდაცია...
return back()->withInput();
});
return redirect()->route('login');
თუ მარშრუტს აქვს პარამეტრები, მათი გადაცემა შეგვიძლია მეთოდის მეორე არგუმენტად :
// მარშრუტი შემდეგი URI-სათვის : /profile/{id}
return redirect()->route('profile', ['id' => 1]);
return redirect()->away('https://www.google.com');
Route::post('/user/profile', function () {
// ...
return redirect('dashboard')->with('status', 'ინფორმაცია წარმატებით განახლდა !');
});
გადამისამართების შემდეგ, წარმოდგენის ფაილში ამ ინფორმაციის გამოყენების სინტაქსი იქნება ასეთი :
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
return response()->json([
'name' => 'Abigail',
'state' => 'CA',
]);
return response()->download($pathToFile);
<!-- resources/views/greeting.blade.php -->
<html>
<body>
<h1>{{ $name }} გამარჯობა ! </h1>
</body>
</html>
წარმოდგენის დაბრუნება შესაძლებელია view დამხმარე ფუნქციის მეშვეობით :
Route::get('/', function () {
return view('greeting', ['name' => 'შამილი']);
});
წარმოდგენის დაბრუნება შესძლებელია View ფასადის მეშვეობითაც :
use Illuminate\Support\Facades\View;
return View::make('greeting', ['name' => 'შამილი']);
დავუშვათ resources/views საქაღალდეში გავაკეთეთ ახალი საქაღალდე templates და წარმოდგენის ფაილები გადავიტანეთ მასში,
მაშინ წარმოდგენის მოთხოვნის სინტაქსი იქნება შემდეგნაირი :
return view('templates.greeting', ['name' => 'შამილი']);
resources/views საქაღალდეში შევქმნათ ახალი საქაღალდე templates და მასში გავაკეთოთ წარმოდგენის ახალი ფაილი template.php. შესაბამისად გადავაკეთოთ view ფუნქციაც :
return view('templates.template')
ამჟამად გვაქვს სტატიკური გვერდი სადაც ლოგიკის არავითარი ელემენტი არ არის და არც ცვლადებია გამოყენებული. გადავცეთ მას რაიმე ცვლადი :
<h1><?php echo $title; ?></h1>
ეს მოგვცემს შეცდომას რადგან $title ცვლადი განსაზღვრული არ არის. ცვლადი უნდა აღვწეროთ კონტროლერში, ან მარშრუტის დამმუშავებელში :
return view('templates.template',['title'=>'Hello World !']);
დავუშვათ გვინდა რამდენიმე ცვლადის, ანუ რამდენიმე ინფორმაციის ერთდროულად გამოყენება, ასეთ შემთხვევაში ხელსაყრელია დავიხმაროთ მასივი :
$data = array('title'=>'Hello World !' , 'title1'=>'Hello World 1');
return view('templates.template',$data);
return view('templates.template')->with('title','Hello World 2 !');
with მეთოდის გამოყენებისას რამდენიმე ცვლადის მიმაგრება ხდება ასე :
public function index()
{
$view = view('templates.template');
$view->with('title','Hello World !');
$view->with('title1','Hello World 1');
$view->with('title2','Hello World 2');
return $view;
}
არსებობს with მეთოდის გამოყენების კიდევ ერთი ვარიანტი :
return view('templates.template')->withTitle('Hello World');
ანუ with მეთოდის დასახელება პრეფიქსად ერთვის ცვლადის დიდი ასოთი დაწყებულ სახელს, შემდეგ კი ფრხილებში ეთითება ცვლადის
მნიშვნელობა.
$firstname = "ვასო";
$lastname = "ნადირაძე";
$age = "30";
$result = compact("firstname", "lastname", "age");
print_r($result); // Array ( [firstname] => ვასო [lastname] => ნადირაძე [age] => 30 )
რაც შეეხება laravel-ში ამ მეთოდის გამოყენების სინტაქსს, იგი შემდეგნაირია :
public function index()
{
$firstname = "ვასო";
$lastname = "ნადირაძე";
$age = "30";
return view('templates.template', compact('firstname', 'lastname', 'age'));
}
ამ ინფორმაციების, წარმოდგენის ფაილში გამოყენების ხერხებს განვიხილავთ შემდეგ თავში.
წარმოდგენის ფაილების გასაფორმებლად უნდა გამოვიყენოთ Laravel ფრეიმვორკის ფუნქციები. მაგალითად ნავიგაციური მენიუს HTML კოდი ხშირად არის შემდეგნაირი :
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">Articles</a></li>
<li><a href="#">Article</a></li>
<li><a href="#">About</a></li>
</ul>
თითოეული ბმული დაკავშირებულია კონკრეტულ გვერდთან. გვერდის უკან კი მოიაზრება კონკრეტული მარშრუტი, ამიტომ ბმულების ფორმირებისას
ისინი უნდა დავაკავშიროთ ამ მარშრუტებთან. ამის გაკეთება კი, როგორც ვიცით, შესაძლებელია route ფუნქციის მეშვეობით :
<ul class="nav navbar-nav">
<li><a href="<?php echo route('home'); ?>">Home</a></li>
<li><a href="<?php echo route('articles'); ?>">Articles</a></li>
<li><a href="<?php echo route('article',array('id'=>10)); ?>">Article</a></li>
<li><a href="<?php echo route('about'); ?>">About</a></li>
</ul>
public function index()
{
if (view()->exists('templates.template'))
{
return view('templates.template')->withTitle('Hello World');
}
}
namespace App\Providers;
use Illuminate\Support\Facades\View;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
//
}
public function boot()
{
View::share('key', 'value');
}
}
შესაძლებელია, რომ ამ პროცესმა უარყოფითი გავლენა იქონიოს აპლიკაციის სისწრაფეზე, ასე რომ view:cache Arttisan ბრძანებით ეგვიძლია დავქეშოთ წარმოდგენის ფაილები :
php artisan view:cache
ქეშირებული შაბლონები შეინახება storage/framework/views საქაღალდეში. ქეშის გასუფთავება კი მოხდება ასე :
php artisan view:clear
კომპილირებული შაბლონები ინახება storage/framework/views საქაღალდეში.
იმისათვის რათა წარმოდგენის ფაილი blade შაბლონიზატორმა დაამუშავოს, ფაილის გაფართოება უნდა იყოს blade.php. გადავარქვათ ჩვენს მიერ შექმნილ ფაილს სახელი:
template.blade.php
ამ ფაილს ახლა უკვე ამუშავებს შაბლონიზატორი Blade.
Route::get('/', function () {
return view('templates.template', ['name' => 'შამილი']);
});
წარმოდგენის ფაილი :
{{ $name }} გამარჯობა !
არ არის აუცილებელი მაინცდამაინც ცვლადის სახით მიმაგრებული ინფორმაცია გამოვიტანოთ ამ გზით, შესაძლებელია PHP-ს ნებისმიერი
ფუნქციის შედეგის გამოტანაც :
მიმდინარე UNIX დროითი ნიშნული არის {{ time() }}
<script>
var app = <?php echo json_encode($array); ?>;
</script>
იგივეს გაკეთება უფრო მარტივადაა შესაძლებელი შაბლონიზატორის @json დირექტივის გამოყენებით :
<script>
var app = @json($array);
</script>
public function index()
{
$script = "<script>alert('Hello')</script>";
return view('templates.template')->with('script', $script);
}
წარმოდგენის ფაილში შევიტანოთ ამდაგვარი ჩანაწერი :
{{ $script }}
ასევე შევქმნათ შესაბამისი მარშრუტი, რომელსაც ეს მეთოდი დაამუშავებს და შევიდეთ შესაბამის ბმულზე (ეს ყველაფერი უკვე ვიცით და ამიტომ
კოდის ნიმუშს აღარ დავწერ). შედეგად ვიხილავთ :
<script>alert('Hello')</script>
ნაგულისხმეობის პრინციპით შაბლონიზატორის {{ }} ჩანაწერი ავტომატურად იყენებს PHP-ს htmlspecialchars ფუნქციას,
XSS შეტევებისაგან თავის დასაცავად. ყველა HTML სიმბოლოს ჩანაცვლება ხდება შესაბამისი ნიშნულებით და HTML კოდი გარდაიქმნება სტრიქონად.
თუ წარმოდგენის ფაილში ამდაგვარ ჩანაწერს შევიტანთ :
{!! $script !!}
ვნახავთ, რომ Javascript-ის alert ფუნქცია მართლაც იმუშავებს.
@if (count($records) === 1)
ერთი ჩანაწერი
@elseif (count($records) > 1)
რამდენიმე ჩანაწერი
@else
არცერთი ჩანაწერი
@endif
მეტი კომფორტულობისათვის არსებობს @unless, @isset და @empty დირექტივებიც :
@unless (Auth::check())
თქვენ არ ხართ სისტემაში შესული
@endunless
@isset($records)
// $records განსაზღვრულია და არ არს null...
@endisset
@empty($records)
// $records ცარიელია
@endempty
@auth
// აუტენტიფიცირებულია
@endauth
@guest
// არააუტენტიფიცირებულია
@endguest
@switch($i)
@case(1)
First case...
@break
@case(2)
Second case...
@break
@default
Default case...
@endswitch
@for ($i = 0; $i < 10; $i++)
მიმდინარე მნიშვნელობა არის {{ $i }}
@endfor
@foreach ($users as $user)
<p>მომხმარებლის ID : {{ $user->id }}</p>
@endforeach
@forelse ($users as $user)
<li>{{ $user->name }}</li>
@empty
<p>მომხმარებლები ვერ მოიძებნა</p>
@endforelse
@while (true)
<p>ჩაციკლვა :))</p>
@endwhile
ციკლებთან მუშაობისას შეიძლება დაგვჭირდეს კონკრეტული იტერაციების გამოტოვება. ასეთ შემთხვევაში დაგვეხმარება @continue and @break
დირექტივები :
@foreach ($users as $user)
@if ($user->type == 1)
@continue
@endif
<li>{{ $user->name }}</li>
@if ($user->number == 5)
@break
@endif
@endforeach
იგივეს გაკეთება შესაძლებელია ასეც :
@foreach ($users as $user)
@continue($user->type == 1)
<li>{{ $user->name }}</li>
@break($user->number == 5)
@endforeach
@foreach ($users as $user)
@if ($loop->first)
პირველი იტერაცია
@endif
@if ($loop->last)
ბოლო იტერაცია
@endif
<p>მომხმარებლის ID : {{ $user->id }}</p>
@endforeach
თუ ვიმყოფებით ჩადგმულ ციკლში, $loop ცვლადის parent თვისების მეშვეობით შეგვიძვლია მივწვდეთ მშობელ ციკლს :
@foreach ($users as $user)
@foreach ($user->posts as $post)
@if ($loop->parent->first)
მშობელი ციკლის პირველი იტერაცია
@endif
@endforeach
@endforeach
$loop ცვლადის თვისებები :
თვიდება | აღწერა |
---|---|
$loop->index | მიმდინარე იტერაციის ინდექსი (იწყება 0-დან). |
$loop->iteration | მიმდინარე იტერაცია (იწყება 1-დან) |
$loop->remaining | დარჩენილი იტერაციების რაოდენობა |
$loop->count | ელემენტების რაოდენობა მასივის გავლისას ციკლში |
$loop->first | ვიყოფებით თუ არა ციკლის პირველ იტერაციაზე |
$loop->last | ვიყოფებით თუ არა ციკლის ბოლო იტერაციაზე |
$loop->even | არის თუ არა ლუწი მიმდინარე იტერაცია |
$loop->odd | არის თუ არა კენტი მიმდინარე იტერაცია |
$loop->parent | მშობელ ციკლთან წვდომა ჩადგმული ციკლიდან |
{{-- ეს კომენტარი არ შევა დაგენერირებულ HTML-ში --}}
<div>
@include('shared.errors')
<form>
</form>
</div>
გარდა იმისა, რომ მშობელ ფაილზე მიმაგრებული ინფორმაცია, მემკვიდრეობით ავტომატურად გადაეცემა შვილობილ ფაილსაც, შეგვიძლია დამატებითი ინფორმაციაც მივამაგროთ
ამ უკანასკნელს :
@include('view.name', ['status' => 'complete'])
როდესაც @include დირექტივის მეშვეობით ფაილის გამოძახებას ვაკეთებთ, Laravel-დააბრუნებს შეცდომას თუ მითითებული ფაილი ვერ მოიძებნება.
ამის თავიდან ასაცილებლად შეგვიძლია გამოვიყენოთ @includeIf დირექტივა :
@includeIf('view.name', ['status' => 'complete'])
თუ გვსურს წარმოდგენის ფაილი გამოვიძახოთ იმისდამიხედვით თუ რა მნიშვბნელობა აქვს მინიჭებული კონკრეტულ ლოგიკურ ოპერატორს. მაშინ უნდა გამოვიყენოთ
@includeWhen and @includeUnless დირექტივები :
@includeWhen($boolean, 'view.name', ['status' => 'complete'])
@includeUnless($boolean, 'view.name', ['status' => 'complete'])
@php
$counter = 1;
@endphp
განვიხილოთ მშობელი შაბლონის მარტივი მაგალითი :
<!-- resources/views/layouts/app.blade.php -->
<html>
<head>
<title>App Name - @yield('title')</title>
</head>
<body>
@section('sidebar')
მშობელი ბლეიდის გვერდითი არე
@show
<div class="container">
@yield('content')
</div>
</body>
</html>
მივაქციოთ ყურადღება @section და @yield დირექტივებს, პირველი მათგანის მეშვეობით ხდება შიგთავსისის კონკრეტული
ფრაგმენტის ანუ სექციების შექმნა, მეორე მათგანი კი უზრუნველჰყოფს ამ სექციების საჭირო ადგილებში გამოტანას.
ახლა შევქმნათ ამ მშობელი შაბლონის მემკვიდრე შაბლონი. ამისათვის გამოიყენება @extends დირექტივა:
<!-- resources/views/child.blade.php -->
@extends('layouts.app')
@section('title', 'გვერდის სათაური')
@section('sidebar')
@parent
<p>ეს არე დაემატება მშობელი შაბლონის გვერდით არეს</p>
@endsection
@section('content')
<p>შვილობილი შაბლონის შიგთავსი</p>
@endsection
<form method="POST" action="/profile">
@csrf
...
</form>
<!-- resources/views/layouts/app.blade.php -->
<html>
<head>
<title>App Name - @yield('title')</title>
</head>
<body>
@section('sidebar')
მშობელი ბლეიდის გვერდითი არე
@show
<div class="container">
@yield('content')
</div>
@stack('scripts')
</body>
</html>
შვილობილი შაბლონი :
<!-- resources/views/child.blade.php -->
@extends('layouts.app')
@section('title', 'გვერდის სათაური')
@section('sidebar')
@parent
<p>ეს არე დაემატება მშობელი შაბლონის გვერდით არეს</p>
@endsection
@section('content')
<p>შვილობილი შაბლონის შიგთავსი</p>
@endsection
@push('scripts')
<script src="/example.js"></script>
@endpush
ასევე შესაძლებელია დასტების თანმიმდევრობის განსაზღვრა :
@push('scripts')
ეს იქნება მეორე დასტა...
@endpush
// Later...
@prepend('scripts')
ეს იქნება პირველი დასტა...
@endprepend
$post = App\Models\Post::find(1);
echo url("/posts/{$post->id}");
// http://127.0.0.1:8000/posts/1
// მიმდინარე URL GET პარამეტრების (query string) გარეშე
echo url()->current();
// მიმდინარე URL GET პარამეტრებთან (query string) ერთად
echo url()->full();
// წინა მოთხოვნის სრული URL (GET პარამეტრებთან (query string) ერთად )
echo url()->previous();
ნებისმიერ ამ მეთოდთან წვდომა შესაძლებელია URL ფასადის მეშვეობითაც :
use Illuminate\Support\Facades\URL;
echo URL::current();
Route::get('/post/{post}', function () {
//
})->name('post.show');
route ფუნქციით ამ მარშრუტის შესატყვისი ბმული დაგენერირდება ასე :
echo route('post.show', ['post' => 1]);
// http://127.0.0.1:8000/post/1
რა თქმა უნდა შესაძლებელია, რომ route ფუნქციას გადავცეთ რამდენიმე პარამეტრი ერთდროულადაც :
Route::get('/post/{post}/comment/{comment}', function () {
//
})->name('comment.show');
echo route('comment.show', ['post' => 1, 'comment' => 3]);
// http://127.0.0.1:8000/post/1/comment/3
ნებისმიერი დამატებითი პარამეტრი, რომელიც აღწერილი არ იქნება მარშრუტის განსაზღვრისას, ბმულს დაემატება GET პარამეტრის
სახით :
echo route('post.show', ['post' => 1, 'search' => 'rocket']);
// http://127.0.0.1:8000/post/1?search=rocket
use App\Http\Controllers\HomeController;
$url = action([HomeController::class, 'index']);
თუ კონტროლერის მეთოდს გადაეცემა მარშრუტის პარამეტრები, შეგვიძლია ისინი აღვწეროთ ასოციაციურ მასივში და ეს მასივი
action ფუნქციას გადავცეთ მეორე პარამეტრად :
$url = action([UserController::class, 'profile'], ['id' => 1]);
'driver' => env('SESSION_DRIVER', 'file'),
ეს არის სესიათა დამუშავების მექანიზმი ნაგულისხმეობის პრინციპით. როგორც ვხედავთ ამ მექანიზმის მნიშვნელობად მითითებულია file, ეს ნიშნავს, რომ
სესიები ინახება კონკრეტულ ფაილებში, კომენტარებში აღწერილია სხვა შესაძლო მნიშვნელობებიც ("cookie", "database", "apc", "memcached", "redis", "array",
memcached არის ერთგვარი პროგრამული უზრუნველყოფა, რომლის მეშვეობითაც ხდება ინფორმაციის ჰეშირებული სახით შენახვა ოპერატიულ მეხსიერებაში).
'lifetime' => env('SESSION_LIFETIME', 120),
ეს არის წუთების რაოდენობა, რომლის ამოწურვის შემდეგაც სესიები გაუქმდდება თუ მომხმარებელი უმოქმედოდ იქნება აპლიკაციაში მთელი ამ ხნის განმავლობაში.
'expire_on_close' => false,
გაუქმდეს თუ არა სესიები ბრაუზერის დახურვისას.
'encrypt' => false,
დაიშიფროს თუ არა სესიაში შენახული ინფორმაცია.
'files' => storage_path('framework/sessions'),
სესიის ინფორმაციები ინახება ამ მისამართზე განთავსებულ ფაილებში.
'table' => 'sessions',
აქ უნდა განისაზღვროს მონაცემთა ბაზის ცხრილის დასახელება იმ შემთხვევაში, თუ driver პარამეტრის მნიშვნელობად ავირჩევთ database-ს.
ანუ სესიის ინფორმაციები შეინახება მბ-ში და კერძოდ აქ მითითებულ ცხრილში.
php artisan session:table
ეს ბრძანება შექმნის მიგრაციას ახალ ფაილს :
database/migrations/2021_05_28_100647_create_sessions_table
ცხრილის სტრუქტურა იქნება ამდაგვარი :
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->text('payload');
$table->integer('last_activity')->index();
});
გავუშვათ მიგრაციის შესრულკების ბრძანება :
php artisan migrate
ახლა გადავაკეთოთ driver პარამეტრის აღწერაც :
'driver' => env('SESSION_DRIVER', 'database'),
ეს ყველაფერი კიდევ არ ნიშნავს, რომ სესიები მბ-ში შეინახება, დავაკვირდეთ ჩანაწერს: როგორც ვხედავთ env ფუნქციას გადაცემული აქვს ორი პარამეტრი.
SESSION_DRIVER პარამეტრი აღნიშნავს, რომ სესიები უნდა შეინახოს .env ფაილში SESSION_DRIVER პარამეტრის მნიშვნელობად მითითებული
მექანიზმის მიხედვით, ამ ფაილში ამ მომენტისათვის კი სავარაუდოდ ეს მდგომარეობაა :
...
SESSION_DRIVER=file
...
ეს იმას ნიშნავს, რომ სესიები ფაილებში ინახება, მიუხედავად იმისა, რომ driver პარამეტრს მეორე არგუმენტად გადაცემული აქვს database,
ამიტომ ჩავასწოროთ .env ფაილიც :
...
SESSION_DRIVER=database
...
public function show(Request $request)
{
$result = $request->session()->get('key','არ არსებობს');
dump($result);
}
სესიაში უჯრა სახელად key არ არსებობს, ამიტომ ბრაუზერში დაგვიბრუნდება "არ არსებობს".
public function show(Request $request)
{
$result = $request->session()->all();
dump($result);
}
public function show(Request $request)
{
$request->session()->put('key','value');
$result = $request->session()->all();
dump($result);
}
ამ კოდის შედეგი იქნება დაახლოებით ამდაგვარი რამ :
public function show(Request $request)
{
if ($request->session()->has('key'))
{
dump("1");
}
else
{
dump("0");
}
}
$request->session()->flash('status', 'შეტყობინება წარმატებით გაიგზავნა !');
ამ ინფორმაციის ნახვა კი ასე შეგვიძლია წარმოდგენის ფაილში :
@if(Session::has('status'))
<p>{{ Session::get('status') }}<p>
@endif
$request->session()->increment('count');
$request->session()->increment('count', $incrementBy = 2);
$request->session()->decrement('count');
$request->session()->decrement('count', $decrementBy = 2);
{{ dump(Session::all()) }}
ამ ფასადის გამოყენება, რა თქმა უნდა შესაძლებელია კონტროლერშიც.
სესიის კონკრეტული უჯრის წასაშლელად გამოიყენება forget მეთოდი, რომელსაც პარამეტრად უნდა გადავცეთ შესაბამისისი უჯრის დასახელება :
Session::forget('key2');
სესიის მთლიანად წასაშლელად გამოიყენება flush მეთოდი, რომელსაც პარამეტრის გადაცემა არ სჭირდება.
Session::flush();
dump(session('key'));
თუ გვსურს, რომ session ფუნქციის მეშვეობით სესიაში შევიტანოთ ახალი მნიშვნელობები :
session(['key' => 'value']);
პირველ რიგში routes/web.php ფაილში შევქმნათ მარშრუტები :
use App\Http\Controllers\PostController;
Route::get('/post/create', [PostController::class, 'create']);
Route::post('/post', [PostController::class, 'store']);
GET მარშრუტი გამოიტანს სიახლის დასამატებელ ფორმას, POST მარშრუტი კი ამ ფორმაში აკრეფილ ინფორმაციას შეინახავს მონაცემთა ბაზაში.
ახლა შევქმნათ კონტროლერი, ამ ეტაპზე მისი store მეთოდი დავტოვოთ ცარიელი:
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* სიახლის დასამატებელი ფორმის გამოტანა
*
* @return \Illuminate\View\View
*/
public function create()
{
return view('post.create');
}
/**
* სიახლის შენახვა მბ-ში
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
// ინფორმაციის ვალიდაცია და შენახვა
}
}
ეს კონტროლერი, ისევე როგორც, ყველა სხვა კონტროლერი, არის Controller კლასის მემკვიდრე. თუ გავხსნით Controller კლასს, ვნახავთ, რომ მასში
აღწერილია შემდეგი კოდი :
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
ეს კონტროლერი კი, თავის მხრივ, არის BaseController მშობელი კლასის მემკვიდრე და იყენებს სამი ტრეიტის,
ანუ დამატებითი კლასის ფუნქციონალს. ჩვენ გვაინტერესებს ტრეიტი ValidatesRequests, სწორედ ამ კლასის დახმარებითაა შესაძლებელი, ამა თუ იმ
კონტროლერში ინფორმაციის ვალიდაცია, ტრეიტი აღწერილია შემდეგ ფაილში vendor/laravel/framework/src/Illuminate/Foundation/Validation/ValidatesRequests.php.
ინფორმაციის ვალიდაციისათვის უნდა მივმართოთ Illuminate\Http\Request კლასის ობიექტის validate მეთოდს :
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);
// ინფორმაცია ვალიდურია...
}
როგორც ვხედავთ ვალიდაციის წესები პარამეტრად გადავეცით validate მეთოდს (ვალიდაციის წესების სრული სია შეგიძლიათ იხილოთ
აქ). თუ ყველა წესი შესრულდება, კონტროლერი
ჩვეულებრივად გააგრძელებს მუშაობას, წინააღმდეგ შემთხვევაში კი შესაბამისი პასუხი დაუყოვნებლივ დაუბრუნდება მომხმარებელს და ვალიდაციის შემდეგ აღწერილი
ინსტრუქციები აღარ შესრულდება.
არსებობს ვალიდაციის წესების გადაცემის სხვაგვარი სინტაქსიც :
$validatedData = $request->validate([
'title' => ['required', 'unique:posts', 'max:255'],
'body' => ['required'],
]);
$request->validate([
'title' => 'bail|required|unique:posts|max:255',
'body' => 'required',
]);
ამ შემთხვევაში თუ title ატრიბუტის unique წესი დაირღვევა, მაშინ max წესის გადამოწმება აღარ მოხდება.
Illuminate\View\Middleware\ShareErrorsFromSession შუამავლის დამსახურებით, $errors ცვლადი ხელმისაწვდომია აპლიკაციის ნებისმიერ წარმოდგენის ფაილში და სწორედ მასში ინახება შეტყობინებები დარღვევების შესახებ ($errors ცვლადი არის Illuminate\Support\MessageBag ფაილში აღწერილი კლასის ობიექტი).
ვალიდაცია აღწერილი გვაქვს store მეთოდში და თუ ვამბობთ, რომ წესების დარღვევისას სისტემა უკან ამისამართებს მომხმარებელს, შესაბამისად გადავალთ create მეთოდში, რომელშიც სიახლის დასამატებელი ფორმის წარმოდგენის ფაილს ვაგენერირებთ, ან ფაილში შეცდომების ნახვა შემდეგნაირად შეგვიძლია :
<!-- /resources/views/post/create.blade.php -->
<h1>სიახლის დამატებაt</h1>
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<!-- სიახლის დასამატებელი ფორმა -->
<!-- /resources/views/post/create.blade.php -->
<label for="title">სიახლის სათაური</label>
<input id="title" type="text" name="title" class="@error('title') is-invalid @enderror">
@error('title')
<div class="alert alert-danger">{{ $message }}</div>
@enderror
$title = $request->old('title');
ასევე შეგვიძლია გამოვიყენოთ გლობალური დამხმარე old :
<input type="text" name="title" value="{{ old('title') }}">
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class PostController extends Controller
{
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);
if ($validator->fails())
{
return redirect('post/create')->withErrors($validator)->withInput();
}
// სიახლის შენახვა...
}
}
withErrors მეთოდი სესიაში შეინახავს შეტყობინებს შეცდომების შესახებ და ასევე, საშუალებას მოგვცემს წარმოდგენის ფაილში გამოვიყენოთ $errors
ცვლადი.
make მეთოდში შესაძლებელია ვალიდაციის შეცდომის შეტყობინებების განსაზღვრაც :
$validator = Validator::make($input, $rules, $messages = [
'required' => ':attribute არის აუცილებელი ველი',
]);
შეტყობინებაში :attribute ჩანაწერი ავტომატურად ჩანაცლდება ველის დასახელებით.
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PostController;
Route::get('/post/create', [PostController::class, 'create']);
Route::post('/post', [PostController::class, 'store']);
შევქმნათ წარმოდგენის ფაილი resources/views/post/create.blade.php :
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('store_post') }}" method="post">
@csrf
<input type="text" name="title" class="@error('title') is-invalid @enderror" value="{{ old('title') }}">
<textarea name="body" class="@error('title') is-invalid @enderror">{{ old('body') }}</textarea>
<input type="submit" value="გაგზავნა">
</form>
PostController :
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function create()
{
return view('post.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|alpha|min:5',
'body' => 'required',
]);
}
}
ფორმის სანახავად შევიდეთ მისამართზე http://127.0.0.1:8000/post/create. ფორმა გავაგზავოთ შემდეგი ვარიანტებით :
იმისათვის რათა, კონკრეტული ველის ყველა შეცდომის შეტყობინება გამოვიტანოთ უნდა გამოვიყენოთ $errors ობიექტის get მეთოდი, რომელსაც პარამეტრად უნდა გადავცეთ ველის დასახელება :
...
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->get('title') as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
</div>
@endif
...
იმის დასაგენად, დაფიქსირდა თუ არა კონკრეტულ ველზე ვალიდაციის შეცდომა, გამოიყენება $errors ობიექტის has მეთოდი,
რომელსაც პარამეტრად უნდა გადავცეთ ასევე ველის დასახელება :
@if($errors->has('email'))
...
@endif
'default' => env('DB_CONNECTION', 'mysql'),
ეს ჩანაწერი განსაზღვრავს თუ მონაცემთა ბაზის მართვის რომელ სისტემასთან ვმუშაობთ.
ამავე ფაილში აღწერილია მბ-სთან დასაკავშირებელი პარამეტრები სხვადასხვა სისტემებისათვის. mysql-ისათვის ეს პარამეტრებია :
...
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
...
როგორც ვხედავთ ზოგიერთი პარამეტრის მნიშვნელობა ბრუნდება env ფუნქციით, ეს ფუნქცია კავშირს ამყარებს .env ფაილთან და სწორედ
იქიდან მოაქვს ინფორმაცია. მივაქციოთ ყურადღება, რომ env ფუნქციას მეორე არგუმენტებად გადაცემული აქვს მნიშვნელობები, რომლებსაც სისტემა
ავტომატურად გამოიყენებს თუ .env ფაილში არ განვსაზღვრავთ შესაბამის პარამეტრებს. შევიტანოთ ცვლილებები .env ფაილში:
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:Y2FMuhHrSmNjAh5NRuHY6NPlVjhl/YDVmHhe115iwXU=
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://localhost
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
ახლა შევქმნათ მიგრაცია, ამისათვის, როგორც ვთქვით, დაგვჭირდება ფრეიმვორკის კონსოლი, გავხსნათ ბრძანებათა კონსოლი და გავუშვათ ბრძანება :
php artisan make:migration create_articles_table
ეს ბრძანება შექმნის მიგრაციის ახალ ფაილს - 2021_06_02_074019_create_articles_table, რომელშიც აღწერილი იქნება შესაბამისი კლასი.
ფაილის დასახელებაში გარდა ჩვენს მიერ მითითებული სათაურისა დამატებულია მიმდინარე თარიღი და მიმდინარე დროის ნიშნული. მიგრაციების ფაილების
ნახვა შესაძლებელია შემდეგ მისამართზე database/migrations. ახლად შექმნილ ფაილში აღწერილი იქნება შემდეგი კლასი :
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateArticlesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('articles');
}
}
როგორვ ვხედავთ, ეს კლასი არის გლობალური კლასის - Migration-ის მემკვიდრე. აღწერილია ორი მეთოდი up() და
down(). პირველ მათგანში მითითებული ინსტრუქციები შესრულდება მაშინ, როდესაც გამოვიძახებთ CreateArticlesTable მიგრაციას,
ხოლო მეორე მათგანის ინსტრუქციები შესრულდება მაშინ, როცა შევწყვეტთ კონკრეტული მიგრაციების გამოყენებას (მაგალითად გავაუქმებთ ბოლოს
გაშვებულ მიგრაციებს).
'php artisan make:migration create_articles_table' ჩანაწერიდან სისტემამ ავტომატურად დაადგინა, რომ ცხრილის შექმნის მიგრაციას ვქმნით და Schema ფასადსაც შესაბამისი 'create' მეთოდით მიმართა.
up() მეთოდი მიმართავს Schema კლასს, ეს არის ცხრილების სპეციალური კონსტრუქტორი, მისი დახმარებით ხდება მბს ცხრილებთან მუშაობა. ამ კლასის create მეთოდი ქმნის ახალ ცხრილს, მეთოდს პირველ პარამეტრად უნდა გადაეცეს ცხრილის სახელი, მეორე პარამეტრი კი არის ქოლბექ ფუნქცია, რომელიც უნდა შესრულდეს ცხრილის შექმნის შემდეგ. ამ ფუნქციას, თავის მხრივ, მითითებული აქვს არგუმენტი, რომლის მეშვეობითაც შეგვიძლია მივმართოთ უშუალოდ ცხრილის ობიექტს.
დავამატოთ რამდენიმე ველი ცხრილს :
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateArticlesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->id(); // id ველი იქნება : INT, AUTO_INCREMENT, PRIMARY KEY
$table->string('name', 100); // name ველი იქნება : Varchar 100
$table->text('text'); // text ველი იქნება : Text
$table->string('img', 255); // img ველი იქნება : Varchar 255
$table->timestamps(); // შეიქმნება timestamp ტიპის ორი ველი created_at და updated_at
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('articles');
}
}
ველთა ტიპების სრული სია შეგიძლიათ იხილოთ აქ.
php artisan migrate
ამ ბრძანების შემდეგ შესრულდება ყველა მიგრაცია, რომელიც database/migrations საქაღალდეშია.
php artisan migrate:rollback
თუ ახლა phpmyadmin-ს შევამოწმებთ იქ დაგვხვდება მხოლოდ ერთი ცხრილი migrations.
php artisan make:migration change_articles_table --table=articles
შეიქმნება ახალი მიგრაცია :
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class ChangeArticlesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('articles', function (Blueprint $table) {
//
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('articles', function (Blueprint $table) {
//
});
}
}
დავამატოთ ველი :
public function up()
{
Schema::table('articles', function (Blueprint $table) {
$table->string('alias', 100); // Varchar 100
});
}
ამასთანავე არ უნდა დაგვავიწყდეს, რომ down() მეთოდში მისათითებელია ინსტრუქცია, რომელიც წაშლის ამ ველს მიმდინარე მიგრაციის
გაუქმების შემთხვევაში. ველების წასაშლელად გამოიყენება $table ობიექტის dropColumn() მეთოდი, რომელსაც პარამეტრად უნდა გადაეცეს
შესაბამისი ველის დსასახელება :
public function down()
{
Schema::table('articles', function (Blueprint $table) {
$table->dropColumn('alias');
});
}
'php artisan make:migration change_articles_table' ჩანაწერიდან სისტემამ ავტომატურად დაადგინა, რომ ცხრილის რედაქტირების მიგრაციას ვქმნით და Schema ფასადსაც შესაბამისი 'table' მეთოდით მიმართა.
ისღა დაგვრჩენია გავუშვათ მიგრაცია შესრულებაზე :
php artisan migrate
use Illuminate\Support\Facades\Schema;
Schema::rename($from, $to);
ცხრილის წასაშლელად უნდა გამოვიყენოთ Schema ფასადის drop ან dropIfExists მეთოდი :
Schema::drop('users');
Schema::dropIfExists('users');
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
//
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
//
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
};
ახლა განვმარტოთ თუ რის გამო მოხდა ეს ცვლილება. განვიხილოთ ასეთი სიტუაცია :
php artisan make:seeder ArticlesSeeder
შეიქმნება ფაილი database/seeders/ArticlesSeeder.php :
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class ArticlesSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
//
}
}
როგორც ვხედავთ კლასი ArticlesSeeder არის Seeder კლასის მემკვიდრე და მას გააჩნია მეთოდი run(), ამ მეთოდში აღწერილი
ინსტრუქციები შესრულდება მაშინ, როცა ავამუშავებთ ჩვენს მიერ შექმნილ მექანიზმს. ამ მეთოდში აღვწეროთ მბ-ს ცხრილში ინფორმაციის შესატანი
ინსტრუქციები, მართალია ჯერ არ ვიცით თუ როგორ უნდა ვიმუშავოთ მბ-სთან, მაგრამ ოდნავ გავუსწროთ მოვლენებს და მოვიყვანოთ მარტივი მაგალითი :
namespace Database\Seeders;
use DB;
use Illuminate\Database\Seeder;
class ArticlesSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
DB::table('articles')->insert([
[
'name' => 'Blog Post 2',
'text' => 'Blog Post 2 testing post 2 and its text',
'img' => 'pic2.jpg'
],
[
'name' => 'Blog Post 3',
'text' => 'Blog Post 3 testing post 3 and its text',
'img' => 'pic3.jpg'
]
]);
}
}
ამის შემდეგ database/seeders/DatabaseSeeder.php ფაილში აღწერილი DatabaseSeeder კლასის run მეთოდში უნდა
ჩავამატოთ ჩვენს მიერ შექმნილი, მბ-ში ინფორმაციის შემტანი ფაილის (ArticlesSeeder.php) შესაბამისი ჩანაწერი, ეს საჭიროა იმისათვის,
რომ ინფორმაციის შეტანის ბრძანების გაშვებისას სისტემამ ეს ფაილიც გამოიძახოს.
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
$this->call(ArticlesSeeder::class);
}
}
ამის შემდეგ საჭიროა, რომ ხელახლა მოვახდინოთ composer-ის ავტოჩამტვირთველის გენერირება, რათა ახალდამატებული კლასიც შევიდეს ჩატვირთული კლასების სიაში,
ეს ხდება შემდეგი ბრძანებით :
composer dump-autoload
ახლა უკვე შეგვიძლია გავუშვათ მბ-ში ინფორმაციის შეტანის ბრძანება :
php artisan db:seed
არსებობს მეორე ვარიანტიც : თუ DatabaseSeeder კლასში არ ჩავამატებთ ზემოთ აღწერილ ჩანაწერს,
მაშინ ინფორმაციის შეტანის ბრძანება უნდა გავუშვათ შემდეგი სახით :
php artisan db:seed --class=ArticlesSeeder
namespace App\Http\Controllers;
use DB;
use App\Http\Controllers\Controller;
class PostController extends Controller
{
public function index()
{
$articles = DB::select("SELECT * FROM articles");
dump($articles);
}
}
ბრძანებას დავამატოთ WHERE ფილტრი, მაგრამ მანამდე აღვნიშნოთ ერთი რამ : laravel-ი მბ-სთან მუშაობისას იყენებს PDO ინტერფეისს, რაც იმას
ნიშნავს, რომ ბრძანებების გაშვება ხდება წინასწარგანსაზღვრის პრინციპით (პრეპარირებული განაცხადები). ასეთ შემთხვევაში შეგვიძლია
select მეთოდს მეორე პარამეტრად, მასივის სახით გადავცეთ ის მნიშვნელობები, რომლებიც
ჩაანაცვლებენ პრეპარირებული განაცხადის ნიშნულებს, მარკერებს :
$articles = DB::select("SELECT * FROM articles WHERE id=?", [2]);
dump($articles);
select მეთოდი აბრუნებს შედეგთა ნაკრებს მასივის სახით, მასივისა, რომლის თითოეული ელემენტიც არის PHP stdClass-ის ობიექტის
სახით წარმოდგენილი კონკრეტული ჩანაწერი მონაცემთა ბაზიდან :
foreach ($articles as $article)
{
echo $article->name;
}
$insert = DB::insert("INSERT INTO articles (name, text, img) VALUES(?,?,?)", ['Article 4','Article 4 Text','img4.jpg']);
dd($inser);
$update = DB::update("UPDATE articles SET name=? WHERE id > ?", ['Renamed Article', 1]);
dd($update);
ეს მეთოდი აბრუნებს ზემოქმედებული ჩანაწერების რაოდენობას.
$delete = DB::delete("DELETE FROM articles WHERE id=?", [1]);
dd($delete);
მეთოდი აბრუნებს წაშლილი ჩანაწერების რაოდენობას.
$statement = DB::statement("DROP TABLE test");
dd($statement); // true/false
$unprepared = DB::unprepared('UPDATE articles SET text = "New text" WHERE id = 2');
dd($unprepared); // true/false
ინგ : Builder - მწარმოებელი, მწარმოებელი-ქარხანა, მშენებელი
მოთხოვნათა კონსტრუქტორის უკან მოიაზრება სპეციალური კლასი Builder (ფსევდონიმი queryBuilder ანუ მოთხოვნათა მშენებელი :)) ), რომელსაც გააჩნია კონკრეტული მეთოდები, რომელთაგანაც თითოეული უზრუნველჰყოფს ბრძანების სრული ტანის კონკრეტული ნაწილის ფორმირებას, ჩვენ აღარ გვიწევს მოთხოვნის ხელით დაწერა. აღნიშნული კლასი აღწერილია vendor/laravel/framework/src/Illuminate/Database/Builder.php ფაილში.იმისათვის რათა გამოვიყენოთ ეს კლასი, პირველ რიგში უნდა შევქმნათ მბ-ს ცარიელი მოთხოვნის ობიექტი კონკრეტული ცხრილისათვის, ამისათვის უნდა მივმართოთ DB ფასადის table მეთოდს, რომელსაც პარამეტრად უნდა გადავცეთ ცხრილის დასახელება.
$articles = DB::table('articles')->get();
dd($articles);
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება
"SELECT * FROM articles"
$article = DB::table('articles')->first();
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT * FROM articles LIMIT 1"
$name = DB::table('articles')->value('name');
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT 'name' FROM articles LIMIT 1"
$names = DB::table('articles')->pluck('name');
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT 'name' FROM articles"
$count = DB::table('articles')->count();
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT COUNT(*) FROM articles"
$max = DB::table('articles')->max('id');
ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT MAX('id') FROM articles"
ასევე არსებობს min, avg და sum მეთოდებიც, რომლებიც ანალოგიურად მუშაობს.
$articles = DB::table('articles')->select('id','name');
თუ ამ ბრძანებას გავუშვებთ, შედეგად დაგვიბრუნდება Builder კლასის ობიექტი და არა ის შედეგი რაც გვსურს, იმიტომ რომ select მეთოდს ჯერ
არ შეუსრულებია თავისი საქმე, ამისათვის მას უნდა მივაშველოთ get მეთოდი :
$articles = DB::table('articles')->select('id','name');
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT id, name FROM articles"
SELECT * FROM articles WHERE id > 10
Laravel-ში კი შემდეგნაირი :
$articles = DB::table('articles')->select('name')->where('id','>',2)->get();
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება
"SELECT 'name' FROM articles WHERE id > 2"
როგორც ვხედავთ where მეთოდს გადაეცა სამი პარამეტრი: იმ ველის დასახელება რომლის მიხედვითაც ვფილტრავთ, პირობითი ოპერატორი და შესადარებელი
მნიშვნერლობა. თუ პირობით ოპერატორტს საერთოდ არ მივუთითებთ ფრეიმვორკი იგულისხმებს, რომ ეს ოპერატორი არის ტოლობის ოპერატორი.
რამდენიმე პირობითი ფილტრის ერთდროულად გამოყენების სინტაქსი ასეთია :
$articles = DB::table('articles')->select('id','name')
->where('id','>',2)
->where('name','like','%A%')
->get();
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT 'name' FROM articles WHERE id > 2 AND name LIKE '%A%'"
როგორც ვხედავთ, პირობითი ოპერატორები ერთმანეთთან დაკავშირდა ლოგიკური "და" -ს მეშვეობით. ჩნდება კითხვა : როგორ მოვიქცეთ თუ გვჭირდება მაგალითად ლოგიკური "ან" ? ამისათვის where ოპერატორს უნდა დავუმატოთ მეოთხე არგუმენტი :
$articles = DB::table('articles')->select('id','name')
->where('id','>',2)
->where('name','like','%A%','or')
->get();
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT 'name' FROM articles WHERE id > 2 OR name LIKE '%A%'"
რამდენიმე პირობითი ოპერატორის გამოყენება შესაძლებელია where მეთოდზე მხოლოდ ერთი მიმართვითაც, ასეთ შემთხვევაში მას არგუმენტად უნდა
გადაეცეს მასივი, რომელიც თავის თავში მოიცავს ფილტრის პირობების შემცველ ქვე-მასივებს :
$articles = DB::table('articles')->select('id','name')
->where([
['id','>',2],
['name','like','a%','or']
])
->get();
$articles = DB::table('articles')->whereBetween('id',[2,5])->get();
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT 'name' FROM articles WHERE id BETWEEN 2 AND 5"
ამ მეთოდის შებრუნებული მეთოდია whereNotBetween.
ამ ორი მეთოდიას ანალოგიურია მეთოდები whereIn და whereNotIn ამიტომ მათ მაგალითებს აღარ მოვიყვანთ.
$articles = DB::table('articles')->get()->groupBy('name');
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT * FROM articles GROUP BY name"
$articles = DB::table('articles')->take(2)->get();
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT * FROM articles LIMIT 2"
$insert = DB::table('articles')->insert([
['name' => 'test name', 'text' => 'test text', 'img' => 'test.jpg'],
['name' => 'test name 1', 'text' => 'test text 1', 'img' => 'test.jpg']
]);
dd($insert);
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ორი ბრძანება :
"INSERT INTO articles (name, text) VALUES ('test name', 'test text', 'test.jpg')"
"INSERT INTO articles (name, text) VALUES ('test name 1', 'test text 1', 'test.jpg')"
მეთოდი აბრუნებს TRUE მნიშვნელობას წარმატების შემთხვევაში, წინააღმდეგ შემთხვევაში ბრუნდება მნიშვნელობა FALSE.
ანალოგიურად მუშაობს insertGetId მეთოდიც, უბრალოდ ის აბრუნებს ბოლოს დამატებული ჩანაწერის id-ს.
$update = DB::table('articles')->where('id',2)->update(['name' => 'hello world']);
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"UPDATE articles SET name='hello world' WHERE id=2"
მეთოდი აბრუნებს ზემოქმედებული ჩანაწერების რაოდენობას.
$delete = DB::table('articles')->where('id',2)->delete();
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"DELETE FROM articles WHERE id=2"
$article = DB::table('articles')->find(3);
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება :
"SELECT * FROM articles WHERE id = 3"
მეთოდი აბრუნებს ზემოქმედებული ჩანაწერების რაოდენობას.
$users = DB::table('users')
->join('contacts', 'users.id', '=', 'contacts.user_id')
->join('orders', 'users.id', '=', 'orders.user_id')
->select('users.*', 'contacts.phone', 'orders.price')
->get();
$users = DB::table('users')->leftJoin('posts', 'users.id', '=', 'posts.user_id')->get();
$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();
ეს ჩანაწერი დააგენერირებდა ასეთ ბრძანებას :
select count(*) as user_count, status from `users` where `status` <> 1 group by `status`
ეს ყველაფერი მოგვცემდა ასეთ შედეგს :
{
"age":32,
"sex":"male",
"salary":4500,
"languages":[
"ka","en"
]
}
{
"age":30,
"sex":"male",
"salary":5500,
"languages":[
"ka"
]
}
{
"age":28,
"sex":"female",
"salary":2500,
"languages":[
"ka","ru","en"
]
}
{
"age":21,
"sex":"male",
"salary":1500,
"languages":[
"ka"
]
}
properties ველის მიხედვით მოვძებნოთ მომხმარებლები, რომელთა ხელფასიც 5500 ლარია :
$users = DB::table('users')->where('properties->salary', 5500)->get();
იგივეს გაკეთება ასეც შეგვიძლია :
$users = DB::table('users')->whereJsonContains('properties->salary', 5500)->get();
თუ ვიყენებთ MySQL ან PostgreSQL მონაცემთა ბაზებს, შეგვიძლია, რომ whereJsonContains მეთოდს სასურველი პარამეტრები გადავცეთ
მასივის სახით :
$users = DB::table('users')->whereJsonContains('properties->languages', ['en', 'ka'])->get();
ახლა გავიგიოთ რომელმა მომხმარებლებმა იციან ორი ენა, ამაში დაგვეხმარება whereJsonLength მეთოდი :
$users = DB::table('users')->whereJsonLength('properties->languages', 2)->get();
ახლა გავიგიოთ რომელმა მომხმარებლებმა იციან ორ ენაზე მეტი :
$users = DB::table('users')->whereJsonLength('properties->languages', '>', 2)->get();
$users = DB::table('users')->whereBetween('id', [1, 100])->get();
$users = DB::table('users')->whereNotBetween('id', [1, 100])->get();
$users = DB::table('users')->whereIn('id', [1, 2, 3])->get();
$users = DB::table('users')->whereNotIn('id', [1, 2, 3])->get();
$users = DB::table('users')->whereNull('updated_at')->get();
$users = DB::table('users')->whereNotNull('updated_at')->get();
$users = DB::table('users')->whereDate('created_at', '2016-12-31')->get();
$users = DB::table('users')->whereMonth('created_at', '12')->get();
$users = DB::table('users')->whereDay('created_at', '31')->get();
$users = DB::table('users')->whereYear('created_at', '2016')->get();
$users = DB::table('users')->whereTime('created_at', '=', '11:20:45')->get();
$users = DB::table('users')->whereColumn('first_name', 'last_name')->get();
შეგვიძლია მეთოდს დავამატოთ მესამე პარამეტრიც :
$users = DB::table('users')->whereColumn('updated_at', '>', 'created_at')->get();
ასევე შეგვიძლია, რომ შედარების პირობები გადავცეთ მასივის სახით :
$users = DB::table('users')
->whereColumn([
['first_name', '=', 'last_name'],
['updated_at', '>', 'created_at'],
])->get();
მოდელის შსაქმნელად კონსოლში უნდა ავკრიფოთ შემდეგი ბრძანება :
php artisan make:model Article
მოდელის სახელის განსაზღვრისას შეზღუდვები არ არსებობს, მაგრამ როგორც წესი მოდელს სახელს არქმევენ ხოლმე მბს იმ ცხრილის დასახელების მიხედვით,
რომელთან სამუშაოდაც უნდა გამოვიყენოთ ეს მოდელი, მაგალითად თუ ცხრილს ჰქვია articles მაშინ სასურველია შესაბამის მოდელს დავარქვათ
Article. ცხრილს სახელი ჰქვია მრავლობით ფორმაში რადგან იგი შეიცავს რამდენიმე article-ს ანუ ჩანაწერის შესახებ ინფორმაციას, მოდელი კი
როგორც ვთქვით მუშაობს ცხრილის კონკრეტულ ჩანაწერებთან, ამიტომ მოდელს დავარქვით ცხრილის სახელი მხოლობით ფორმაში.
მოდელები ინახება app/Models საქაღალდეში, ბრძანების შედეგად შექმნილი მოდელიც შეინახება აქ.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
//
}
ჩვენს მიერ შექმნილი ნებისმიერი მოდელი იქნება Model მშობელი კლასის მემკვიდრე, რომელიც აღწერილია შემდეგ ფაილში :
vendor/laravel/framework/src/Illuminate/Database/Model.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $table = 'articles';
}
უნდა აღინიშნოს, რომ ამ შემთხვევაში $table თვისების განსაზღვრა საჭირო არ არის, რადგანაც ცხრილისა და მოდელის
სახელებს შორის გვაქვს ის დამოკიდებულება რაც ზემოთ ვთქვით : მოდელს სახელად აქვს ცხრილის დასახელების მხოლობითი
ფორმა. ასეთ შემთხვევაში კი სისტემა ავტომატურად მიხვდება, თუ რომელ ცხრილთან უნდა იმუშავოს.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $primaryKey = 'article_id';
}
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
public $incremeting = FALSE;
}
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
public $timestamps = FALSE; // ამ შემთხვევაში ეს ველები შეივსება მნიშვნელობა Null-ით
}
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $fillable = ['name','text','img'];
}
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $guarded = ['name','text','img'];
}
namespace App\Http\Controllers;
use App\Models\Article;
use App\Http\Controllers\Controller;
class PostController extends Controller
{
public function index()
{
foreach (Article::all() as $article)
{
echo $article->name;
}
}
}
all() მეთოდმა რეალურად გაუშვა შემდეგი ბრძანება :
SELECT * FROM articles
შედეგად კი დააბრუნა მოდელთა კოლექცია, რას ნიშნავს ეს ? როგორვ ვთქვით, მოდელი არის ობიექტის სახით აღწერილი, ცხრილის კონკრეტული ჩანაწერი,
კონკრეტული ჩანაწერის აბსტრაქცია, აქედან გამომდინარე, თუ ცხრილში რამდენიმე ჩანაწერია, დაბრუნდება რამდენიმე ჩანაწერის ობიექტური წარმოდგენა ანუ ამ
წარმოდგენათა (მოდელთა) ნაკრები.
ჩნდება კითხვა, როგორ გამოვიყენოთ მიღებული ინფორმაცია ანუ ცხრილის ველთა მნიშვნელობები ? ეს ხდება მოდელის იმავე სახელწოდებების მქონე თვისებების მიხედვით რაც აქვს ცხრილთა ველებს :
...
foreach (Article::all() as $article)
{
echo $article->name;
}
...
რასაკვირველია მოდელში შესაძლებელია მთხოვნათა კონსტრუქტორის გამოყენებაც :
$a$articles = Article::where('id', '>', 2)->orderBy('name')->take(3)->get();
// მოდელის ამოღება პირველადი გასაღების მიხედვით (primary key)
$article = Article::find(1);
// პირველივე ისეთი მოდელის ამოღება, რომელიც აკმაყოფილებს მითითებულ პირობებს
$article = Article::where('status', 1)->first();
// პირველივე ისეთი მოდელის ამოღება, რომელიც აკმაყოფილებს მითითებულ პირობებს
$article = Article::firstWhere('active', 1);
შეიძლება მოხდეს ისე, რომ დაგვჭირდეს კონკრეტული ჩანაწერის ამოღება და მისი დაბრუნება თუ იგი მოიძებნება,
წინააღმდეგ შემთხვევაში კონკრეტული ფუნქციონალის შესრულება :
$model = Article::where('id', 65673)->firstOr(function () {
echo 'ვერ მოიძებნა';
});
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
class PostController extends Controller
{
public function index()
{
try
{
$article = Article::findOrFail(31);
// მოდელი მოიძებნა...
}
catch (ModelNotFoundException $e)
{
if ($e instanceof ModelNotFoundException)
{
dd($e->getMessage()); // No query results for model [App\Models\Article] 31
}
}
}
}
$article = Article::firstOrCreate([
'name' => 'Article Name',
'text' => 'Article Text',
]);
მუშაობის პროცესის გასამარტივებლად ხშირად იყენებენ ხოლმე მიდგომას, რომლის მიხედვითაც კონკრეტული მოდელების შესაქმნელად საჭირო ინფორმაცია ერთ სივრცეშია ხოლმე
მოქცეული (მაგალითად მასივში როგორც ზემოთ მოვიქეცით) და მოდელისათვის კონკრეტული ველების ცალ-ცალკე მიკუთცნება აღარ არის საჭირო. ამ მიდგომას ეწოდება
მასიური განსაზღვრებადობა (Mass Assignment), რომელიც, რიგ შემთხვევებში, არც ისე უსაფრთხო შეიძლება იყოს.
ნახსენები მასივის როლში ხშირად შეიძლება მოგვევლინოს HTTP მოთხოვნის ტანი ($request->all()). წარმოვიდგინოთ ასეთი შემთხვევა: დავუშვათ მომხმარებლების ცხრილში გვაქვს 'admin' ველი, რომლის შესაძლო მნიშვნელობებიცაა 0 (არ არის ადმინსტრატორი) ან 1 (ადმინსტრატორია). ბუნებრივია მომხმარებლებს არ უნდა მივცეთ ამ ველის შეცვლის უფლება. არადა თუ იგი ფორმაში ახალ ველს ჩაამატებს სახელით - admin და მნიშვნელობით 1, რა თქმა უნდა ეს ველი HTTP მოთხოვნის ტანშიც შევა და შესაბამისად მომხმარებელი არალეგალური გზით გაიხდის თავს ადმინად )) :
// მომხარებლის დამატების კოდი
$user = new User(request()->all());
ამ ყველაფრის თავიდან ასაცილებლად Laravel-ში შემოღებულია შევსებადი და დაცული ველების ცნებები.
დავუბრუნდეთ ჩვენს კოდს :
$article = Article::firstOrCreate([
'name' => 'Article Name',
'text' => 'Article Text',
]);
თუ ახლა ამ კოდსს გავუშვებთ ვიხილავთ შეცდომას, უფრო სწორად ფრეიმვორკის მიერ დაგენერირებულ გამონაკლისს (MassAssignmentException), ეს იმიტომ, რომ Article
მოდელში, $fillable მეთოდის მეშვეობით არ არის აღწერილი ცხრილის იმ ველთა დასახელებები, რომლებშიც ნებადართულია ინფორმაციის შეტანა
მასიური განსაზღვრებადობის გზით :
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $fillable = ['name', 'text'];
}
ინფორმაციის შეტანამდე firstOrCreate მეთოდი გადაამოწმებს უკვე ხომ არ არსებობს ცხრილში ისეთი ჩანაწერი რომლისთვისაც name ველის მნიშვნელობა არის
'Article Name', თუ ესეთი ჩანაწერი არსებობს მაშინ მეთოდი დააბრუნებს ამ ჩანაწერის მოდელს. ხოლო თუ არ არსებობს ასეთი ჩანაწერი, მაშინ მეთოდი შექმნის მას
და დააბრუნებს ახალშექმნილ მოდელს.
$article = new Article;
$article->name = 'ტესტი';
$article->text = 'ტესტი';
$article->img = 'ტესტი';
$article->save();
save მეთოდით ჩანაწერების დამატებისას ავტომატურად ხდება ცხრილის created_at და updated_at ველების შევსება.
$article = Article::create([
'name' => 'სათაური',
'text' => 'ტექსტი',
'img' => 'test.jpg',
]);
create() მეთოდით ჩანაწერების შექმნისას მოდელში აუცილებლად უნდა გვქონდეს აღწერილი მასიურ განსაზღვრებადობასთან დაკავშირებული
fillable და guarded თვისებები :
protected $fillable = ['name', 'text','img'];
ან :
protected $fillable = [];
$article = Article::find(3);
$article->name = "ახალი სათაური";
$article->save();
updated_at ველის მნიშვნელობა ავტომატურად განახლდება.
Article::where('id','>',1)->update([
'name' => 'ახალი სათიურები',
'text' => 'ახალი ტექსტები'
]);
$article = Article::find(4);
$article->delete();
Article::destroy(9);
თუ რამდენიმე ჩანაწერის წაშლა გვსურს ერთდროულად, მაშინ მეთოდს პარამეტრად უნდა გადაეცეს მასივი სადაც შეტანილი იქნება ამ ჩანაწერთა იდენტიფიკატორები.
php artisan make:migration change_article_table_soft --table=articles
იმისათვის რათა მიგრაციაში განვსაზღვროთ აღნიშნული ველი, მიგრაციის up მეთოდში უნდა გამოვიყენოთ softDeletes მეთოდი. სწორედ ეს მეთოდი
დაამატებს ცხრილში deleted_at ველს. შესაბამისად მიგრაციის down მეთოდში მოვახდინოთ მისი უგულებელყოფა :
...
public function up()
{
Schema::table('articles', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('articles', function (Blueprint $table) {
$table->dropColumn('deleted_at');
});
}
...
ახლა გავუშვათ ამ მიგრაციის შესრულების ბრძანება :
php artisan migrate
თუ ახლა შევამოწმებთ articles ცხრილს, ვნახავთ, რომ მას დამატებული ექნება deleted_at ველი.
ახლა გამოვიყენოთ softDelete() მეთოდი. ამისათვის მოდელში უნდა დავამატოთ სპეციალური კლასი softDeletes, ეს კლასი მდებარეობს შემდეგ მისამართზე : vendor/laravel/framework/src/Illuminate/Database/Eloquent/SoftDeletes.php.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Article extends Model
{
use SoftDeletes;
protected $fillable = ['name', 'text'];
}
კონტროლერში კი ხდება შემდეგი :
$article = Article::find(10);
$article->delete();
თუ ახლა შევამოწმებთ ცხრილს ვნახავთ რომ id=10 ჩანაწერი განახლებული იქნება და deleted_at ველში მითითებული იქნება წაშლის თარიღი.
როგორც ვხედავთ ეს ჩანაწერი წაშლილი არ არის მაგრამ იგი აღარ შევა შედეგთა ნაკრებში, თუ ამოვარჩევთ მაგალითად ცხრილის ყველა ჩანაწერს.
იმის დასადგენად წაიშლა თუ არა ჩანაწერი, გამოიყენება trashed მეთოდი :
$article = Article::find(8);
$article->delete();
if ($article->trashed())
{
die('წაიშლა');
}
წაშლილი ჩანაწერის აღსადგენად გამოიყენება withTrashed და restore მეთოდები :
Article::withTrashed()->find(10)->restore();
onlyTrashed მეთოდის დახმარებით ხდება softDelete მეთოდით წაშლილი ჩანაწერების ამოღება :
$articles = Article::onlyTrashed()->get();
მაგალითად გვაქვს სტატიების ცხრილი articles და იმ მომხმარებლების ცხრილი - users, რომლებიც ამატებენ ამ სტატიებს. ასეთ შემთხვევაში, როგორც წესი, მომხმარებლის საიდენტიფიკაციო ნომერი (id) იწერება ხოლმე articles ცხრილის user_id ველში. ანუ articles ცხრილის user_id ველი არის users ცხრილთან კავშირის საგარეო გასაღები. პრაქტიკაში ასეთი კავშირები საკმაოდ ხშირია და ამიტომ laravel-ში ჩადგმულია ფუნქციონალი, რომელიც ამარტივებს ამ კავშირებთან მუშაობას.
laravel-ში არსებობს ცხრილთა შორის კავშირის რამდენიმე ვარიანტი, მათ შორის ძირითადებია :
Laravel-ის ინსტალაციის შემდეგ database/migrations საქაღალდეში ავტომატურად შეიქმნებოდა მიგრაციის რამდენიმე ფაილი, მათ შორის users ცხრილის შესაქმნელი xxxx_xx_xx_xxxxxx_create_users_table. შესაბამისად, როდესაც პირველად გავუშვით 'php artisan migrate' ბრძანება, ეს ცხრილიც შეიქმნებოდა.
ახლა შევქმნათ ტელეფონის ნომრების ცხრილი :
php artisan make:migration create_phones_table
აღვწეროთ ველები :
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePhonesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('phones', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('phone', 100);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('phones');
}
}
გავუშვათ მიგრაციის ბრძანება :
php artisan migrate
ვიხილავთ ამდაგვარ შეტყობინებას :
ახლა კი ყურადღება მივაქციოთ მიგრაციის შემდეგ ჩანაწერს :
$table->foreignId('user_id')->constrained()->onDelete('cascade');
foreignId მეთოდი პარამეტრად გადაცემულ მნიშვნელობას - 'user_id'-ს, მოხსნის '_id' ბოლოსართს, მიღებულ სიტყვას დაამატებს მრავლობითი ფორმის აღმნიშვნელ
's' ასოს და ამგვარად მიიღებს სიტყვა 'users' - ს, რაც ნიშნავს, რომ phones ცხრილის user_id ველი უკავშირდება users ცხრილის id ველს.
constrained()->onDelete('cascade') ჩანაწერი კი აღნიშნავს, რომ users ცხრილიდან კონკრეტული მომხმარებლის წაშლის შემთხვევაში, მისი შესაბამისი ტელეფონის ნომერიც წაიშლება phones ცხრილში.
ახლა შევქმნათ ტელეფონების მოდელი :
php artisan make:model Phone
იმისათვის რათა User მოდელი დავაკავშიროთ Phone მოდელთან, User მოდელში უნდა ჩავსვათ phone მეთოდი,
რომელიც, თავის მხრივ, გამოიძახებს hasOne მეთოდს და დააბრუნებს შესაბამის შედეგს. hasOne მეთოდი აღწერილია
Illuminate\Database\Eloquent\Model მშობელ მოდელში.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* მომხმარებელთან დაკავშირებული ტელეფონის ნომრის ამოღება
*/
public function phone()
{
return $this->hasOne(Phone::class);
}
}
ახლა გამოვიყენოთ დამყარებული კავშირი :
$user = User::find(1); // select * from users where id=1 limit 1
dump($user->phone); // select * from phones where user_id = 1 and user_id is not null limit 1
მას შემდეგ რაც User მოდელში, hasOne() მეთოდის მეშვეობით განვსაზღვრეთ კავშირი 'ერთი-ერთთან', მოდელთან მიმართებაში
შეგვიძლია გამოვიყენოთ დინამიური ანუ ცვალებადსახელიანი მეთოდი, რომლის სახელიც ემთხვევა
User მოდელის იმ მეთოდის სახელს, რომელშიც მოხდა კავშირის განსაზღვრა.
სისტემა საგარეო გასაღებს (foreign key) ადგენს მშობელი კლასის სახელიდან გამომდინარე, მაგალიოთად ამ შემთხვევაში იგი ავტომატურად გულისხმობს, რომ Phone მოდელს აქვს ველი user_id. თუ გვსურს, რომ ეს მიდგომა გადავფაროთ, hasOne მეთოდს, მეორე პარამეტრად უნდა გადავცეთ ჩვენთვის სასურველი ველის დასახელება :
return $this->hasOne(Phone::class, 'foreign_key');
ამ მომენტისათვის, User მოდელი დაკავშირებულია მოდელ Phone-სთან, მაგრამ უკუკავშირი არ არის დამყარებული. ამის გასაკეთებლად
Phone მოდელში ჩავამატოთ user მეთოდი, რომელშიც გამოვიყენებთ belongsTo() მეთოდს (ინგ: belongs - კუთვნილება,
ეკუთვნის) :
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Phone extends Model
{
/**
* მომხმარებელი რომელსაც ეკუთვნის ტელეფონის კონკრეტული ნომერი
*/
public function user()
{
return $this->belongsTo(User::class);
}
}
ამ შემთხვევაში სისტემა საგარეო გასაღებს (foreign key) ადგენს ურთიერთკავშირის აღმწერელი მეთოდის დასახელებითა და '_id' სუფიქსის კომბინაციით. ამ შემთხვევაში მეთოდის დასახელებაა - 'user', სუფიქსთან ერთად კი მიიღება 'user_id', შესაბამისად სისტემა ჩათვლის, რომ Phone მოდელს აქვს ველი 'user_id'. თუ გვსურს, რომ ეს მიდგომა გადავფაროთ, belongsTo მეთოდს, მეორე პარამეტრად უნდა გადავცეთ ჩვენთვის სასურველი ველის დასახელება :
public function user()
{
return $this->belongsTo(User::class, 'foreign_key');
}
ტელეფონის კონკრეტული ნომრის მფლობელის დადგენა შესაძლებელია ასე :
$phone = Phone::find(1); // select * from phones where id = 1 limit 1
dump($phone->user); // select * from users where id = 1 limit 1
php artisan make:migration change_articles_table
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeArticlesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('articles', function (Blueprint $table) {
$table->foreignId('user_id')->constrained()->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$table->dropColumn('user_id');
}
}
ბუნებრივია, რომ ერთმა მომხმარებელმა შესაძლებელია დაამატოს რამდენიმე სიახლე, შესაბამისად users ცხრილი სიახლეების ცხრილ -articles-თან
დაკავშირდება კავშირის ტიპით - ერთი მრავალთან.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* მომხმარებელთან დაკავშირებული ტელეფონის ნომრის ამოღება
*/
public function phone()
{
return $this->hasOne(Phone::class);
}
/**
* მომხმარებელის მიერ დამატებული სიახლეების ამოღება
*/
public function articles()
{
return $this->hasMany(Article::class);
}
}
როგორც ვხედავთ, გამომდინარე იქედან, რომ ერთი მომხმარებელი შეიძლება იყოს რამდენიმე სიახლის ავტორი,
მეთოდის დასახელება მრავლობით ფორმაშია გასაზღვრული.
კონკრეტული მომხმარებლის სიახლეებთან წვდომა შესაძლებელია ასე :
$articles = User::find(1)->articles;
foreach ($articles as $article)
{
//
}
'User::find(1)->articles' ჩანაწერის შედეგად გაეშვებოდა ორი ბრძანება :
select * from users where id = 1 limit 1
select * from articles where user_id = 1 and user_id is not null and deleted_at is null
მეორე ბრძანებას წითლად მონიშნული ჩანაწერი მიემატა იმის გამო, რომ ჩვენ ადრე Article მოდელში გამოვიყენეთ
ე.წ 'მსუბუქი წაშლის სისტემა (softDeletes).
ეს ჩანაწერი :
dump(User::find(1)->articles());
მოგვცემს HasMany ობიექტს :
$article = User::find(1)->articles->where('id',3)->first();
როგორც ვნახეთ, users და articles ცხრილებს შორის დამყარდა კავშირი ერთი ბევრთან.
ახლა განვსაზღროთ უკუკავშირიც, ამისათვის სიახლეების მოდელში ჩავამატოთ შემდეგი ჩანაწერი :
public function user()
{
return $this->belongsTo(User::class);
}
თუ ვამბობთ, რომ უნდა განვსაზღროთ 'ერთი ბევრთან' კავშირის უკუკავშირი, ბუნებრივია ამ კავშირის ფორმულირება
იქნება 'ბევრი ერთთან', სწორედ ამიტომაა მეთოდის დასახელება განსაზღვრული
მხოლობით ფორმაში.
დავაბრუნოთ იმ სიახლის ავტორის სახელი, რომლის id-იც არის 3
$article = Article::find(3);
return $article->user->name;
$articles = Article::all();
dump($articles);
შესრულდება შემდეგი ბრძანება და ბრუნდება მოდელების შემდეგი კოლექცია :
$articles = Article::all();
foreach($articles as $article)
{
echo $article->user->name . '<br>';
}
ასეთ შემთხვევაში მივიღებთ შემდეგ სურათს :
ასეთ შემთხვევაში გამოიყენება ე.წ 'ძუნწი ჩატვირთვა' :)) ამ შემთხვევაში დამატებითი ინფორმაციის ჩატვირთვა ხდება კოლექციასთან ერთად. ოპტიმიზირებული ჩატვირთვისას უნდა გამოვიყენოთ მეთოდი with, რომელიც უზრუნველჰყოფს ინფორმაციის ჩატვირთვას ძირითად ცხრილთან დაკავშირებული სხვა ცხრილებიდანაც. მეთოდს პარამეტრად უნდა გადაეცეს იმ მოდელის დასახელება, რომელთან დაკავშირებაც გვსურს :
$articles = Article::with('user')->get();
foreach($articles as $article)
{
echo $article->user->name . '<br />';
}
ასეთ შემთხვევაში მივიღებთ შემდეგ სურათს :
$users = User::has('articles')->get();
foreach($users as $user)
{
echo $user->name . '<br />';
}
has მეთოდთან ერთად შესაძლებელია სხვადასხვა პირობითი ოპერატორების გამოყენებაც, დავუშვათ გვინდა ამოვარჩიოთ ის მომხმარებლები, რომლებსაც ორზე მეტი სიახლე აქვთ დამატებული:
$users = User::has('articles','>=', 2)->get();
foreach($users as $user)
{
echo $user->name . '<br />';
}
php artisan make:migration create_roles_table
...
public function up()
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
...
php artisan make:migration create_role_user_table
...
public function up()
{
Schema::create('role_user', function (Blueprint $table) {
$table->id();
$table->integer('user_id')->unsigned()->default(1);
$table->foreign('user_id')->references('id')->on('users');
$table->integer('role_id')->unsigned()->default(1);
$table->foreign('role_id')->references('id')->on('roles');
$table->timestamps();
});
}
...
მივაქციოთ ყურადღება, რომ ბოლოს შექმნილი ცხრილის დასახელება 'role_user' შემთხვევითი არ არის - მისი პირველი ნაწილი
'role' არის 'roles' ცხრილთან სამუშაო მოდელის დასახელება, მეორე ნაწილი 'user' კი არის 'users'
ცხრილთან სამუშაო მოდელის დასახელება.
გავუშვათ ამ მიგრაციების შესრულების ბრძანება და შევქმნათ ცხრილები.
php artisan make:model Role
მომხმარებლების მოდელში კი განვსაზღვროთ შემდეგი კავშირი ცხრილებს შორის : ერთი მომხმარებელი უკავშირდება რამდენიმე
როლს :
...
public function roles()
{
return $this->belongsToMany(Role::class);
}
...
ახლა თუ სასურველ კონტროლერში შევიტანთ შემდეგ კოდს :
$user = User::find(1);
$roles = $user->roles;
foreach ($roles as $role)
{
echo $role->name . '<br>';
}
ბრაუზერში ვიხილავთ ჩვენს მიერ დამატებული ორივე როლის დასახელებას (admin, moderator).
ახლა მაგალითისათვის ამოვარჩიოთ მომხმარებლის ის როლი რომლის საიდენტიფიკაციო ნომერიცაა 2 :
$user = User::find(1);
$role = $user->roles()->where('roles.id',2)->first();
dump($role);
მივაქციოთ ყურადრება, რომ 'where' ფილტრში დაგვჭირდა იმის დაკონკრეტება, თუ რომელი ცხრილის საიდენტიფიკაციო
ნომრის მიხედვით ვცდილობთ ინფორმაციის ამოღებას, ეს იმიტომ მოხდა, რომ 'id' ველი სამივე ცხრილშია
(users, roles, role_user) და თუ ასე არ მოვიქცეოდით დაფიქსირდებოდა კონფლიქტი, შეცდომა.
ზუსტად ანალოგიურად მოხდება უკუკავშირის დამყარება : "ერთი როლი უკავშირდება რამდენიმე მომხმარებელს".
$user = User::find(1);
$article = new Article([
'name' => 'მესამე სიახლე',
'text' => 'მესამე სიახლის ტექსტი'
]);
$user->articles()->save($article);
როგორც ვხედავთ save მეთოდს არგუმენტად გადაეცა იმ ინფორმაციის შემცველი მოდელი,
რომლის შეტანაც გვსურს ბაზაში.
რამდენიმე ჩანაწერის ერთდროულად დამატებისათვის გამოიყენება მეთოდი saveMany :
$user = User::find(1);
$user->articles()->saveMany([
new Article(['name'=>'მეხუთე სიახლე', 'text'=>'მეხუთე სიახლის ტექსტი']),
new Article(['name'=>'მეექვსე სიახლე', 'text'=>'მეექვსე სიახლის ტექსტი'])
]);
არსებობს ინფორმაციია შეტანის კიდევ ერთი მეთოდი create, რომელსაც პარამეტრად გადაეცემა უშუალოდ შესატანი ინფორმაცია და არა ამ ინფორმაციის შემცველი მოდელი :
$user = User::find(1);
$user->articles()->create([
'name' => 'მეოთხე სიახლე',
'text' => 'მეოთხე სიახლის ტექსტი'
]);
$user = User::find(1);
$user->articles()->where('id', 3)->update([
'name' => 'მესამე სიახლის ახალი სათაური'
]);
ამ ჩანაწერში უნდა გავითვალისწინოთ ერთი რამ : როგორც ვხედავთ ჯერ ვიღებთ მომხმარებლის შესახებ ინფორმაციას (id=1) და
შემდეგ ვუშვებთ ინფორმაციის განახლების შესახებ ბრძანებას. თუ სიახლე რომლის შეცვლაც გვსურს (id=3), არ ეკუთვნის
ამორჩეულ მომხმარებელს, მაშინ სისტემა არ განაახლებს ამ ჩანაწერს, და ეს ასეც უნდა იყოს.
აუტენტიფიკაციის პარამეტრები განსაზღვრულია config/auth.php ფაილში. მომხმარებლის აუტენტიფიკაციის პროცესი შედგება ორი ძირითადი ელემენტისაგან: პირველი ეს არის ე.წ მცველი - guard, მეორე კი - პროვაიდერი provider.
მცველი განსაზღვრავს თუ რა სახით იქნას შენახული ინფორმაცია იმ მომხმარებლის შესახებ, რომელიც აკეთებს მოთხოვნას, ანუ როგორ შევინახოთ ინფორმაცია იმის შესახებ, რომ მოთხოვნა გააკეთა მაგალითად აუტენტიფიცირებულმა მომხმარებელმა, ეს ინფორმაცია შესაძლებელია შენახულ იქნას სესიაში ან სპეციალურ სტრიქონში სახელად token.
პროვაიდერი განსაზღვრავს თუ როგორ და რა სახით შეიძლება მიიღოს მომხმარებელმა ინფორმაცია მბ-დან ან სხვა წყაროდან.
შ აუტენტიფიკაციის სისტემასთან მუშაობისთვის საჭიროა წარმოდგენის ფაილები, მარშრუტები და კონტროლერები. არსებობს ამ ყველაფრის შექმნის რამდენიმე ვარიანტი. განვიხილოთ ერთ-ერთი მათგანი - Laravel BreezeLaravel Breeze-ს ინსტალაცია ხდება შემდეგი ბრძანების მეშვეობით :
composer require laravel/breeze --dev
ამის შემდეგ უნდა გავუშვათ Artisan-ის ბრძანება :
php artisan breeze:install
ამ ყველაფრის შედეგად ვიხილავთ ამდაგვარ შეტყობინებებს :
Breeze scaffolding installed successfully.
Please execute the "npm install && npm run dev" command to build your assets.
იმისათვის რათა ხელი მიგვიწვდებოდეს CSS სტილებთან საჭიროა შემდეგი ბრძანებების გაშვება :
npm install
npm run dev
ამ მომენტისათვის აუტენტიფიკაციის შაბლონების სტილები ჩვენთვის ნაკლებად მნიშვნელოვანია, მაგრამ თუ მაინც გსურთ, რომ
გქონდეთ ავტორიზაციის ლამაზი ფორმა, მაშინ დააინსტალირეთ node.js (იხილეთ
Typescript-ის ცნობარის მე-2-ე თავი) და გაუშვით ზემოთ
მოყვანილი ორი ბრძანება :))
Breeze-ს ინსტალაციის შემდეგ, შეიქმნებოდა routes/auth.php ფაილი, რომელშიც აღწერილი იქნება აუტენტიფიკაციის სისტემის მარშრუტები და რომლის გამოძახებაც ავტომატურად მოხდებოდა routes/web.phpფაილში :
require __DIR__.'/auth.php';
კონტროლერები განთავსდებოდა App/Http/Controllers/Auth საქაღალდეში.
წარმოდგენის ფაილები განთავსდებოდა resources/views/auth საქაღალდეში.
ისღა დაგვრჩენია ვესტუმროთ http://127.0.0.1:8000/login და http://127.0.0.1:8000/register მისამართებს.
Laravel-ის ინსტალაციის შემდეგ ავტომატურად შეიქმნებოდა მომხმარებლებთან სამუშო App/Models/User მოდელი და ასევე მიგრაციის ორი ფაილი შემდეგი ცხრილებისათვის : users და password_resets. სწორედ users ცხრილში შეინახება რეგისტრაციისას მომხმარებლის მიერ აკრეფილი ინფორმაციები. password_resets ცხრილს კი შევეხებით ოდნავ მოგვიანებით.
routes/web.php ფაილს თუ გადავამოწმებთ, შევნიშნავთ, რომ Breeze-ს ინსტალაციის შემდეგ, მასში ასევე ჩაემატებოდა შემდეგი ჩნაწერი :
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');
ეს არის რეგისტრირებული მომხმარებლის პირადი კაბინეტის მარშრუტი, რომელსაც მიმაგრებული აქვს auth
შუამავალი. ბუნებრივია მომხმარებლს კაბინეტში შესვლა შეუძლია მხოლოდ მაშინ, როდესაც მას აუტენტიფიკაცია გავლილი აქვს
და სწორედ ამას ემსახურება ეს შუამავალიც. ამაში ადვილად დავრწმუნდებით თუ /dashboard გვერდზე შევალთ
აუტენტიფიკაციის გარეშე. ასეთ შემთხვევაში სისტემა გადაგვამისამართებს /login გვერდზე.
დავაკვირდეთ app/Http/Kernel.php ფაილის შემდეგ ფრაგმენტს :
...
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
...
];
...
use Illuminate\Support\Facades\Auth;
// აუტენტიფიცირებული მომხმარებელი
$user = Auth::user(); // App\Models\User Object
// აუტენტიფიცირებული მომხმარებლის ID
$id = Auth::id(); // 6
user მეთოდი აბრუნებს აუტენტიფიცირებული მომხმარებლის ობიექტს, id მეთოდი კი აუტენტიფიცირებული
მომხმარებლის იდენტიფიკატორს.
აუტენტიფიცირებულ მომხმარებელთან წვდომა შესაძლებელია Illuminate\Http\Request კლასის მეშვეობითაც :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TestController extends Controller
{
public function index(Request $request)
{
dd($request->user());
}
}
use Illuminate\Support\Facades\Auth;
if(Auth::check())
{
echo 'აუტენტიფიცირებულია';
}
else
{
return redirect()->route('login');
}
მიუხედავად იმისა, რომ ამ მეთოდით შესაძლებელია მომხმარებლის აუტენტიფიცირება/არააუტენტიფიცირების გადამოწმება,
უკეთესი ვარიანტია თუ საჭირო მარშრუტებს შუამავლების მეშვეობით დავიცავთ ხოლმე და თუ მომხმარებელი ამ შუამავალს
გაივლის, შესაბამისად აღარც იმის გადამოწმება დაგვჭირდება არის თუ არა იგი აუტენტიფიცირებული.
Route::get('/profile', function () {
// მხოლოდ აუტენტიფიცირებული მომხმარებლები
})->middleware('auth');
protected function redirectTo($request)
{
return route('somewhere');
}
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function authenticate(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::attempt($credentials))
{
$request->session()->regenerate();
return redirect()->intended('dashboard');
}
return back()->withErrors(['email' => 'არასწორი ელ_ფოსტა']);
}
}
რა თქმა უნდა, საჭიროა მარშრუტის განსაზღვრაც და ვინაიდან აქამდე Laravel Breeze პაკეტს ვიყენებდით შევიტანოთ
შესაბამისი ცვლილებები routes/auth.php ფაილშიც :
use App\Http\Controllers\LoginController;
Route::post('/login', [LoginController::class, 'authenticate'])->middleware('guest');
attempt მეთოდს პარამეტრად გადაეცემა მასივი, რომელშიც თავმოყრილია აუტენტიფიკაციისათვის საჭირო ინფორმაციები,
ამ შემთხვევაში - ელ_ფოსტა და პაროლი (ინგ: Attempt - გასინჯვა, შემოწმება, გამოცდა, მცდელობა).
ამის შემდეგ მონაცემთა ბაზაში მოიძებნება მომხმარებლის მიერ აკრეფილი ელ_ფოსტის
შესაბამისი ჩანაწერი (email ველის მიხედვით). თუ ასეთი ჩანაწერი მოიძებნება მაშინ უკვე დარდება ამ ჩანაწერის password
ველისა და მომხმარებლის მიერ აკრეფილი პაროლის მნიშვნელობები. როგორც ვიცით password ველში ჰეშირებული
პაროლია შენახული, თუმცა ეს იმას არ ნიშნავს, რომ აუტენტიფიკაციისას ჩვენც უნდა დავჰეშოთ აკრეფილი პაროლი - სისტემა
ამას ავტომატურარდ გააკეთებს. თუ პაროლიც დაემთხვა მაშინ მოხდება აუტენტიფიცირებული მომხმარებლის შესახებ ინფორმაციის
სესიაში შენახვა. მეთოდი აბრუნებს ლოგიკურ მნიშვნელობას - true, თუ აუტენტიფიკაცია წარმატებულია, წინააღმდეგ შემთხვევაში
ბრუნდება მნიშვნელობა - false.
intended მეთოდი მომხმარებელს გადაამისამართებს იმ მარშრუტზე, რომელთან წვდომასაც იგი ცდილობდა აუტენტიფიკაციამდე (ინგ: Intended - განზრახული, ჩაფიქრებული).
თუ გვსურს, რომ attempt მეთოდს დავუმატოთ სხვა გადასამოწმებელი პარამეტრებიც, უნდა მოვიქცეთ ასე :
if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1]))
{
// წარმატებული აუტენტიფიკაცია
}
use Illuminate\Support\Facades\Auth;
$remember = $request->has('remember');
if (Auth::attempt(['email' => $email, 'password' => $password], $remember))
{
// მომხმარებელი დამახსოვრებულია ...
}
როდესაც მომხმარებელი აუტენტიფიკაციისას აწვება 'დამიმახსოვრე' ღილაკს, იკვრება მოქმედებათა შემდეგი ჯაჭვი :
გენერირდება უნიკალური ჰეშირებული სტრიქონი, რომელიც ინახება როგორც ბრაუზერში (Cookie-ს სახით), ასევე სერვერზე
('users' ცხრილის 'remember_token' ველში), ამის შემდეგ თუ მომხმარებელი დახურავს და ისევ გახსნის ბრაუზერს,
სისტემა გადაამოწმებს არსებობს თუ არა დამახსოვრებული ჰეშ-სტრიქონი Cookie-ში, თუ კი - მაშინ შეამოწმებს შეესაბამება
თუ არა ეს სტრიქონი ბაზაში არსებულ რომელიმე ჩანაწერს და თუ ეს ასეა მაშინ მომხმარებელი ავტომატურად ხდება
აუტენტიფიცირებული. Cookie-ში შენახული ინფორმაცია იარსებებს მანამ, სანამ მომხმარებელი არ გამოვა სისტემიდან (logout),
უბრალოდ ბრაუზერის დახურვით ეს ინფორმაცია არ იშლება.
use App\Models\User;
use Illuminate\Support\Facades\Auth;
$user = User::find(1);
Auth::login($user);
Auth::login($user, $remember = true);
იმისათვის რათა login მეთოდმა იმუშავოს, საჭიროა, რომ მომხმარებლის ობიექტი დაკავშირებული იყოს
Illuminate\Contracts\Auth\Authenticatable კონტრაქტთან, სწორედ ამას ემსახურება, Laravel-ში ნაგულისხმევად
შექმნილი app/Models/User მოდელის შემდეგი ჩანაწერი :
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
...
}
Auth::loginUsingId(1, $remember = true);
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
როგორც წესი, კონტროლიორების განსაზღვრა ხდება ხოლმე Gate ფასადთან მიმართვით App\Providers\AuthServiceProvider პროვაიდერის boot მეთოდში. მაგალითისათვის შევქმნათ კონტროლიორი, რომელიც გადაწყვეტს აქვს თუ არა კონკრეტულ მომხმარებელს კონკრეტული სიახლის დარედაქტირების უფლება :
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot()
{
$this->registerPolicies();
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
}
კონტროლიორის გამოყენება ხდება Gate ფასადის allows და denies მეთოდების მეშვეობით.
შევნიშნოთ, რომ კონტროლიორს პარამეტრად არ გადავცემთ აუტენტიფიცირებულ მომხმარებლს, რადგან ამას Laravel-ი
ავტომატურად გააკეთებს :
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PostController extends Controller
{
public function update(Request $request, Post $post)
{
if (! Gate::allows('update-post', $post))
{
abort(403);
}
// სიახლის განახლება ...
}
}
თუ გვსურს, რომ კონკრეტულ ქმედებაზე გადავამოწმოთ, არა აუტენტიფიცირებული, არამედ სხვა მომხმარებლის უფლებები, მაშინ
უნდა გამოვიყენიოთ Gate ფასადის forUser მეთოდი :
if (Gate::forUser($user)->allows('update-post', $post))
{
// მომხმარებელს შეუძლია განაახლოს სიახლე ...
}
if (Gate::forUser($user)->denies('update-post', $post))
{
// მომხმარებელს არ შეუძლია განაახლოს სიახლე ...
}
ასევე შესაძლებელია, რომ გადავამოწმოთ მომხმარებლის უფლებები ერთდროულად რამდენიმე ქმედებაზე. ამისათვის გამოიყენება
Gate ფასადის any და none მეთოდები :
if (Gate::any(['update-post', 'delete-post'], $post))
{
// მომხმარებელს შეუძლია სიახლის წაშლა ან რედაქტირება ...
}
if (Gate::none(['update-post', 'delete-post'], $post))
{
// მომხმარებელს არ შეუძლია სიახლის არც წაშლა და არც რედაქტირება ...
}
/lang
/en
messages.php
/ka
messages.php
ფრეიმვორკის დაინსტალირების შემდეგ ამ საქაღალდეში, ნაგულისხმეობის პრინციპით, იქმნება ლოკალიზაციის ერთადერთი საქაღალდე
ინგლისური ენისათვის lang/en.
როგორც წესი, თითოეული ლოკალიზაცია ინახება საქაღალდეში, რომლის დასახელებაც ემთხვევა შესაბამისი ენის კოდს (მაგ: ინგლისური - en, ქართული - ka და ა.შ). ლოკალიზაციის ქვე-საქაღალდეებში ინახება ე.წ ფაილი ლექსიკონები, მათში აღწერილია კონკრეტული სტრიქონების თარგმანები კონკრეტულ ენაზე. ამ სტრიქონებს ეწოდებათ ენობრივი კონსტანტები. ფაილ ლექსიკონებში ბრუნდება უბრალო ასოციაციური მასივები, რომლის გასაღებებიც გადასათარგმნი სტრიქონებია, ხოლო ამ გასაღებთა მნიშვნელობები - ამ სტრიქონების თარგმანები შესაბამის ენაზე.
თითოეული ფაილი ლექსიკონი განკუთვნილია, პროექტის კონკრეტული ელემენტისათვის, მაგალითად ვალიდაციის ელემენტს აქვს თავისი ფაილი (validation.php), გვერდების გადანომრვის ელემენტს - თავისი (pagination.php), აუტენტიფიკაციის გვერდს - თავისი (auth.php) და ა.შ.
ამის შემდეგ უბრალოდ უნდა გავხსნათ ახლადშექმნილ ლოკალიზაციაში არსებული ლექსიკონ-ფაილები და მათში აღწერილ მასივებში, გასაღებების მნიშვნელობები მივუთითოთ ქართულად.
ეს რაც შეეხებოდა უკვე არსებულ ლექსიკონ-ფაილებს. ახლა შევქმნათ საკუთარი ლექსიკონ-ფაილი და ვნახოთ თუ როგორ ხდება მისი გამოყენება, lang/ka საქაღალდეში შევქმნათ ფაილი - messages.php :
return [
'welcome' => 'კეთილი იყოს თქვენი მობრძანება',
'hello' => 'მოგესალმებით'
];
ლოკალიზაციის შექმნის შემდეგ საჭიროა config/app.php ფაილში აღწერილი ასოციაციური მასივის locale
გასაღების მნიშვნელობის ჩასწორება :
'locale' => 'ka'
ამავე მასივის fallback_locale გასაღებში მითითებულია ალტერნატიული ლოკალიზაციის დასახელება :
'fallback_locale' => 'en'
ანუ თუ locale გასაღებში მითითებული ლოკალიზაცია რაიმე მიზეზის გამო მიუწვდომელი იქნება სისტემისათვის, მაშინ
ფრეიმვორკი შეეცდება გამოიყენოს ალტერნატიული ლოკალიზაცია.
use Lang;
$title = Lang::get('messages.welcome');
როგორც ვხედავთ, მეთოდს პარამეტრად გადაეცა ჯერ სასურველი ფაილ-ლექსიკონის (messages) დასახელება, შემდეგ კი იმ
ენობრივი კონსტანტის დასახელება (welcome), რომლის გადათარგმნაც გვსურს და რომელიც ამავე ფაილშია აღწერილი.
Lang::get ჩანაწერი შეიძლება ჩაიწეროს უფრო მოკლედაც. შემდეგი ორი ჩანაწერი ერთმანეთის ტოლფასია :
$title = Lang::get('messages.welcome');
$title = trans('messages.welcome');
შეიძლება, მოხდეს ისე, რომ ენობრივი კონსტანტის თარგმანი იყოს დინამიური, მაგალითად სხვადსხვა შემთხვევაში შეიძლება
დაგვჭირდეს სხვადასხვა ტექსტები :
კეთილი იყოს თქვენი მობრძანება გიორგი
კეთილი იყოს თქვენი მობრძანება მარიამ
კეთილი იყოს თქვენი მობრძანება ცოტნე
ასეთ შემთხვევაში ენობრივი კონსტანტის თარგმნისას ორწერტილით უნდა გამოიყოს თარგმანის სტატიკური და დინამიური ნაწილები
(ორწერტილის შემდეგ არ უნდა იყოს გამოტოვებული ადგილი), ჩვენს შემთხვევაში სტატიკურია ტექსტი -
"კეთილი იყოს თქვენი მობრძანება", ხოლო სახელები დინამიურია :
return [
'welcome' => 'კეთილი იყოს თქვენი მობრძანება :name',
'hello' => 'მოგესალმებით'
];
ამის შემდეგ Lang ფასადის get მეთოდს, მასივის სახით, არგუმენტად უნდა გადავცეთ სასურველი სახელი :
use Lang;
$title = Lang::get('messages.welcome', array('name' => 'ვასო'));
იმის გასარკვევად აღწერილია თუ არა ესა თუ ის ენობრივი კონსტანტა ამა თუ იმ ფაილ-ლექსიკონში, გამოიყენება Lang
ფასადის has მეთოდი, რომელსაც არგუმენტებად უნდა გადაეცეს საურველი ფაილ-ლექსიკონისა და ენობრივი კონსტანტის
დასახელებები :
use Lang;
if(Lang::has('messages.welcome'))
{
$title = Lang::get('messages.welcome', array('name' => 'ვასო'));
}
use Illuminate\Support\Facades\App;
$locale = App::currentLocale();
if (App::isLocale('ka'))
{
//
}
ვებ-პროგრამირების სფეროში მომუშავე ნებისმიერ ადამიანს, ალბათ ერთხელ მაინც ექნება ყური მოკრული ამდაგვარი გამონათქვამებისათვის :
დასაწყისში ამ ყველაფრის შემდეგ ჩნდება საპასუხო კითხვები :
ამ თავში შევეცდებით პასუხი გავცეთ ამ კითხვებს, შემდეგ ორ თავში კი შევქმნით ტესტირებაზე ორიენტირებულ ორ პროექტს შემდეგი დასახელებებით :
ტესტირებაზე ორიენტირებული დეველოპმენტი (TDD - Test-driven development) არის პროგრამული უზრუნველყოფის შექმნის პროცესი, რომელიც დაფუძნებულია, სატესტო ფუნქციებად ანუ სატესტო შემთხვევებად (test cases) დაფორმატებულ კონკრეტულ პროგრამულ ამოცანებზე და რომლის დროსაც ეს დაფორმატება ხდება პროგრამის საბოლოო იერსახის შექმნამდე.
კი, გეთანხმებით - ცოტა ჩახლართული განმარტებაა :)) მოვიყვანოთ კონკრეტული მაგალითი: დავუშვათ ვქმნით პროექტს, რომლის მეშვეობითაც უნდა შეგვეძლოს მონაცემთა ბაზაში ჩავწეროთ, განვაახლოთ და წავშალოთ ინფორმაციები სხვადასხვა პროდუქტის შესახებ, ანუ გვაქვს სტანდარტული CRUD სისტემა. უფრო კონკრეტულად კი გვაქვს შემდეგი ამოცანები :
იმისათვის, რომ ეს პროექტი გავხადოთ ტესტირებაზე ორიენტირებული, ბუნებრივია უნდა შევქმნათ ამ ამოცანებთან დაკავშირებული ტესტები, ანუ მოვახდინოთ ამ ამოცანების ტესტებად აღწერა. მაშ ასე, შევქმნათ შემდეგი ტესტები :
უშუალოდ ტესტების შინაარსს რაც შეეხება, ამ მომენტისათვის მხოლოდ ზოგადი მონახაზის აღწერით შემოვიფარგლოთ : კონკრეტულ ტესტში უნდა მოვახდინოთ ამ ტესტთან დაკავშირებული ფუნქციის, ანუ პროექტის კონკრეტული ამოცანის შესრულების 'სიმულაცია' : მაგ: test_create_product ტესტში უნდა მივმართოთ პროდუქტის შექმნის მარშრუტს, შემდეგ გადავცეთ მას შესანახი ინფორმაცია, შემდეგ მივმართოთ შესაბამის მოდელს და ა.შ.
ამის შემდეგ სისტემას უნდა ვუთხრათ, რომ შეასრულოს ჩვენი ტესტები. თუ გავითვალისწინებთ, რომ ამ დროისათვის კოდის არცერთი ხაზი გვაქვს დაწერილი, სისტემა ბუნებრივია გვეტყვის :
ვუჯერებთ სისტემას, ვქმნით მარშრუტს და ხელახლა ვუშვებთ ტესტირების ბრძანებას, ამჯერად სისტემა გვეუბნება შემდეგს :
და ა.შ, ნაბიჯ-ნაბიჯ მივუყვებით ფუნქციონალის გამართვას ანუ კოდის წერას. ეს პროცესი გარანტიაა იმისა, რომ ფუნქციონალის სწორად მუშობისათვის საჭირო არცერთი ფრაგმენტი გამოგვრჩება. გარდა ამისა, მას შემდეგ რაც ყველა საჭირო ტესტს აღვწერთ, მხოლოდ და მხოლოდ ერთი ბრძანების მეშვეობით შეგვეძლება, პროექტის სრული ფუნქციონალის გატესტვა, რასაც, თავის მხრივ, აქვს შემდეგი დადებითი მხარეები :
ინგ: Unit - ერთეული, ნაწილი, აგრეგატი, სექცია, ნასკვი, კვანძი;
ფრაგმენტული ტესტირება (unit testing) არის პროგრამული უზრუნველყოფების ტესტირების ტიპი, რომელშიც ხდება პროგრამის ინდივიდუალური ერთეულების, ფრაგმენტების ტესტირება. მისი მიზანია დამტკიცდეს, რომ პროგრამული კოდის თითოეული ფრაგმენტი გამართულად მუშობს. ფრაგმენტული ტესტები იწერება დეველოპერის მიერ, პროექტზე მუშაობის პროცესში. ფრაგმენტის უკან შეიძლება მოიაზრებოდეს კონკრეტული ფუნქცია, მეთოდი, მოდული და ა.შ
ინგ : Suite - ნაკრები, კომპლექტი;
ეს ელემენტი შეიძლება იყოს ერთი ან რამდენიმე testsuite ელემენტის მშობელი.კომპიუტერულ მეცნიერებებში არსებობს ასეთი ცნება - კოდის დაზღვეულობა (code coverage), რომელიც განსაზღვრავს პროგრამული კოდის გატესტვის ხარისხს ტესტების კონკრეტულ ნაკრებთან მუშაობისას. რაც მეტია ეს ხარისხი, მით უფრო ნაკლებია შანსი იმისა, რომ პროგრამაში დაფიქსირდეს ხარვეზები, სხვა სიტყვებით თუ ვიტყვით - რაც უფრო მასშტაბურად იტესტება კოდი, მით უფრო საიმედოა იგი.
PHPUnit-ისათვის <coverage> ელემენტი არის ერთგვარი ფილტრი, რომლის მეშვეობითაც იგი ხვდება თუ რომელი და რა ტიპის ფაილები უნდა მოხვდეს კოდის დაზღვევის არეში.
ხელის საშრობიც და ნაგვის ყუთიც ცალ-ცალკე მშვენივრად ასრულებს თავის საქმეს (წარმოვიდგინოთ, რომ მათ უკან მოიაზრება კონკრეტული ფუნქციის კონკრეტული მეთოდები, რომლებიც უკვე გატესტილი გვაქვს ფრაგმენტული ტესტირების გზით (unit tests)). მაგრამ, როდესაც მათ ერთად ვათავსებთ, რაღაც რიგზე ვერაა :))
სწორედ ეს იდეა დევს ფუნქციონალურ ტესტებშიც - იმის ნაცვლად, რომ აპლიკაციის კონკრეტული ასპექტები ვტესტოთ, ვახდენთ იმ მთლიანი ოპერაციის იმიტაციას, რომელიც შეიძლება, რომ მომხმარებელმა ჩვენს აპლიკაციაში განახორციელოს.
პროექტის ინსტალაციის შემდეგ ავტომატურად იქმნება ერთი ფრაგმენტული და ერთი ფუნქციონალური ტესტი, ერთი და იგივე დასახელებით - ExampleTest. ტესტების გაშვება შესაძლებელია რამდენიმენაირად :
php artisan test
vendor/bin/phpunit
Windows ოპერაციული სისტემის ბრძანებათა ველის შემთხვევაში :
vendor\bin\phpunit
php artisan make:test UserTest
ამ ბრძანებით შექმნილი ტესტი შეინახება tests/Feature საქაღალდეში.
თუ გვსურს, რომ ტესტი tests/Unit საქაღალდეში შეიქმნას, make:test ბრძანებას უნდა მივამატოთ --unit ჩანაწერი :
php artisan make:test UserTest --unit
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_a_basic_request()
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
get მეთოდი ახდენს GET ტიპის მოთხოვნის ინსცენირებას აპლიკაციაში, assertStatus მეთოდი კი ადასტურებს, რომ მიღებულ პასუხს (response)
აქვს სტატუს-კოდი - 200. ეს ტესტი წარმატებით შესრულდება რადგან, როგორც ვიცით, ლარაველის პროექტის ინსტალაციისას routes/web.php
ფაილში ავტომატურად იქმნება შემდეგი მარშრუტი :
Route::get('/', function () {
return view('welcome');
});
მაგრამ თუ ასე მოვიქცევით :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_a_basic_request()
{
$response = $this->get('/some-unknown-url');
$response->assertStatus(200);
}
}
ტესტი წარუმატებელი იქნება და მივიღებთ ამდაგვარ შედეგს :
გარდა assertStatus მეთოდისა, ლარაველში გვხვდება საკმაოდ ბევრი მტკიცებითი ტიპის მეთოდები, რომლებსაც ეტაპობრივად გავეცნობით (ინგ: Assert - მტკიცება, განცხადება, დაცვა, პრეტენზია).
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_interacting_with_the_session()
{
$response = $this->withSession(['banned' => false])->get('/');
}
}
რაც შეეხება აუტენტიფიკაციას - იმისათვის, რომ ტესტირეიბის რეჟიმმა რომელიმე მომხმარებელი აუტენტიფიცირებულად აღიქვას, უნდა გამოვიყენოთ actingAs მეთოდი
(ინგ: Acting - მოვალეობების შემსრულებელი) :
namespace Tests\Feature;
use App\Models\User;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_an_action_that_requires_authentication()
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->withSession(['banned' => false])
->get('/');
}
}
დავწეროთ ტესტი, რომელიც /api/user ბმულზე გააგზავნის POST ტიპის მოთხოვნას და ასევე დაადასტურებს, რომ პასუხად მიღებული JSON-ის კონკრეტული ველის მნიშვნელობა შეესაბამება კონკრეტულ მაჩვენებელს :
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_making_an_api_request()
{
$response = $this->postJson('/api/user', ['name' => 'Sally']);
$response
->assertStatus(201)
->assertJson([
'created' => true,
]);
}
}
აქვე უნდა აღინიშნოს, რომ JSON პასუხს შეგვიძლია მივწვდეთ როგორც მასივს :
$this->assertTrue($response['created']);
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_a_welcome_view_can_be_rendered()
{
$view = $this->view('welcome', ['name' => 'Taylor']);
$view->assertSee('Taylor');
}
}
თუ გვსურს, რომ დავაბრუნოთ კონკრეტული წარმოდგენის ფაილის შიგთავსი, უნდა მოვიქცეთ ასე :
$contents = (string) $this->view('welcome');
$response->assertCookie($cookieName, $value = null);
$response->assertCookieExpired($cookieName);
$response->assertCookieNotExpired($cookieName);
$response->assertCookieMissing($cookieName);
$response->assertCreated();
$response->assertForbidden();
$response->assertJson(array $data, $strict = false);
$response->assertNoContent($status = 204);
$response->assertNotFound();
$response->assertOk();
$response->assertRedirect($uri);
$response->assertRedirectToSignedRoute($uri);
$response->assertSessionHas($key, $value = null);
$response->assertSessionHasErrors(['name', 'email']);
$response->assertSessionHasNoErrors();
$response->assertStatus($code);
$response->assertSuccessful();
$response->assertUnauthorized();
$response->assertViewHas($key, $value = null);
$response->assertViewHasAll(array $data);
$response->assertViewIs($value);
$this->assertAuthenticated($guard = null);
$this->assertGuest($guard = null);
$this->assertAuthenticatedAs($user, $guard = null);
ამისათვის გამოიყენება Illuminate\Foundation\Testing\RefreshDatabase ტრეიტი :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
public function test_basic_example()
{
$response = $this->get('/');
// ...
}
}
RefreshDatabase ტრეიტის გამოყენებისას, ტესტების ყოველ გაშვებაზე ასევე სრულდება მიგრაციებში აღწერილი ინსტრუქციებიც.
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi',
'remember_token' => Str::random(10),
];
}
}
როგორც უკვე აღვნიშნეთ, მოდელმწარმოებელი არის ბაზაში ინფორმაციის შეტანის ერთ-ერთი საშუალება. ნებისმიერი მოდელმწარმოებელი არის ლარაველის
ძირითადი მოდელმწარმოებლის Illuminate\Database\Eloquent\Factories\Factory მემკვიდრე და გააჩნია მეთოდი
definition რომელშიც ხდება ნაგულისხმები მნიშვნელობების მინიჭება მოდელის ველებისათვის
(ინგ: Definition - განმარტება, განსაზღვრა, გარკვეულობა).
მნიშვნელობების დაგენერირება ხდება fake დამხმარეს მეშვეობით, რომელიც დაკავშირებულია შემთხვევითი ინფორმაციების მაგენერირებელ PHP ბიბლიოთეკასთან, სახელწოდებით - Faker (ინგ: Fake - გაყალბება, ფალსიფიცირება, მოტყუება, თაღლითობა :))) ).
დავუბრუნდეთ ისევ ტესტებს. ტესტში მოდელმწარმოებლის გამოყენება შესაძლებელია შემდეგნაირად :
use App\Models\User;
public function test_models_can_be_instantiated()
{
$user = User::factory()->create();
// ...
}
$this->assertDatabaseCount('users', 5);
$this->assertDatabaseHas('users', [
'email' => 'vasil.nadiradze@gmail.com',
]);
$this->assertDatabaseMissing('users', [
'email' => 'vasil.nadiradze@gmail.com',
]);
ამ თავში გავაკეთებთ ვინილის შესაკვეთ, ტესტსირებაზე ორიენტირებულ MVC აპლიკაციას შემდეგი საბაზისო ფუნქციონალით :
პროექტში ვიმუშავებთ შემდეგ თემებსა და საკითხებზე :
composer create-project laravel/laravel vinyl
როდესაც ტესტირებაზე ორიენტირებული პროექტის შექმნას ვიწყებთ, პირველი რაც თავში უნდა გვიტრიალებდეს არის ის, რომ ჯერ უნდა დავწეროთ ტესტები და შემდეგ
უშუალოდ პროექტის ფუნქციონალის კოდი. კი, მაგრამ როგორ გავტესტოთ ისეთი რამ, რაც ჯერ არ არსებოს ? იდეა იმაში მდგომარეობს, რომ ტესტის წერისას უნდა წარმოვიდგინოთ
თითქოს კოდი უკვე არსებობს და შეგვიძლია ტესტში მივმართოთ მას. ბუნებრივია ტესტი წარმატებით არ შესრულდება და ვიხილავთ შეტყობინებას შეცდომის შესახებ.
ამავე შეტყობინებაში იქნება განსაზღვრული ჩვენი შემდეგი ნაბიჯი. შევასრულოთ ეს ინსტრუქცია და ისევ გავუშვათ ტესტი, ეს ყველაფერი გავიმეოროთ მანამ, სანამ
ტესტი წარმატებით არ გაეშვება.
დასაწყისში ეს ყველაფერი გაუგებრად და ბუნდოვნად მოგვეჩვენება, მაგრამ დავწერთ რამოდენიმე ტესტს და ყველაფერი დალაგდება ^_^ :)
php artisan make:test SearchTest
ეს ბრძანება /tests/Feature საქაღალდეში შექმნის SearchTest.php ფაილს :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class SearchTest extends TestCase
{
/**
* A basic feature test example.
*
* @return void
*/
public function test_example()
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
ეს კოდი გატესტავს ხელმისაწვდომია თუ არა აპლიკაციის მთავარი გვერდი, ანუ მთვარ გვერდზე შესვლისას ვღებულობთ თუ არა 200 HTTP სტატუსს.
vendor/bin/phpunit
შედეგი იქნება ამდაგვარი :
PHPUnit 9.5.25 #StandWithUkraine
... 3 / 3 (100%)
Time: 00:00.200, Memory: 22.00 MB
OK (3 tests, 3 assertions)
/tests/Feature და /tests/Unit საქაღალდეებში წავშალოთ ExampleTest.php ფაილები, რომლებიც პროექტის ინსტალაციისას ავტომატურად შეიქმნებოდა.
თუ გვსურს კონკრეტული ტესტის გაშვება მაშინ უნდა გამოვიყენოთ --filter ჩანაწერი მივუთითოთ კლასის მეთოდის დასახელება :
vendor/bin/phpunit --filter SearchTest
vendor/bin/phpunit --filter test_example
პირველ რიგში დავწეროთ ტესტი, რომელიც შეამოწმებს ხელმისაწვდომია თუ არა პროექტის მთავარი გვერდი (სწორედ მთავარ გვერდზე გამოვიტანთ ვინილების ჩამონათვალსა და საძიებო ფორმას ოდნავ მოგვიანებით). ტესტის კლასში მეთოდის დაწერის ორი ვარიანტი არსების, პირველი - test ანოტაციის დახმარებით :
/** @test */
public function vinyl_search_page_is_accessible()
{
$this->get('/')->assertOk();
}
და მეორე - test_ პრეფიქსის დახმარებით :
public function test_vinyl_search_page_is_accessible()
{
$this->get('/')->assertOk();
}
ტესტის დასახელების განსაზღვრისას შევეცადოთ ხოლმე, რომ დასახელებამ მაქსიმალურად კარგად აღწეროს თუ რა ხდება ამ ტესტში ანუ რას ვტესტავთ.
შევქმნათ მარშრუტი, რომელიც აპლიკაციის მთავარ გვერდზე შესვლისას ჩატვირთავს search.blade.php წარმოდგენის ფაილს :
// routes/web.php
Route::get('/', function () {
return view('search');
});
ტესტში კი გვაქვს ასეთი სიტუაცია :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class SearchTest extends TestCase
{
/** @test */
public function vinyl_search_page_is_accessible()
{
$this->get('/')->assertOk();
}
}
გავუშვათ ტესტი. ვიხილავთ ამდაგვარ შეტყობინებას :
1) Tests\Feature\SearchTest::vinyl_search_page_is_accessible
Expected response status code [200] but received 500.
Failed asserting that 200 is identical to 500.
The following exception occurred during the last request:
InvalidArgumentException: View [search] not found.
ამ მომენტიდან მოყოლებული, რაც უნდა გავაკეთოთ არის ის, რომ მინიმალური ძალისხმევით გამოვასწოროთ არსებული მდგომარეობა, მინიმალური ძალისხმევა გულისხმობს
მხოლოდ და მხოლოდ ამ კონკრეტული შეტყობინებს გამოსწორებას რასაც მოცემულ მომენტში ვხედავთ და არაფერს სხვას. სისტემა გვეუბნება, რომ არ გვაქვს წარმოდგენის ფაილი,
შევქმნათ იგი : /resources/views/search.blade.php
ამის შემდეგ დავწეროთ ტესტი, რომელიც შეამოწმებს გვაქვს თუ არა ყველა საჭირო ინფორმაცია ძებნის გვერდზე, ანუ გვაქვს თუ არა პროდუქტის ჩამონათვალი. სწორედ აქ ერთვება საქმეში ტესტების სამფაზიანი ზოგადი სტრუქტურა, ეს ფაზებია :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\Product;
class SearchTest extends TestCase
{
/** @test */
public function vinyl_search_page_is_accessible()
{
$this->get('/')->assertOk();
}
/** @test */
public function vinyl_search_page_has_all_the_required_page_data()
{
// მომზადების ფაზა
Product::factory()->count(3)->create(); // შევქმნათ 3 ვინილი
}
}
გავუშვათ ტესტი. ვიხილავთ ამდაგვარ შეტყობინებას :
1) Tests\Feature\SearchTest::vinyl_search_page_has_all_the_required_page_data
Error: Class "App\Models\Product" not found
შევქმნათ მოდელი :
php artisan make:model Product
გავუშვათ ტესტი. ვიხილავთ ამდაგვარ შეტყობინებას :
1) Tests\Feature\SearchTest::vinyl_search_page_has_all_the_required_page_data
Error: Class "Database\Factories\ProductFactory" not found
შევქმნათ მოდელმწარმოებელი :
php artisan make:factory ProductFactory
გავუშვათ ტესტი :
1) Tests\Feature\SearchTest::vinyl_search_page_has_all_the_required_page_data
Illuminate\Database\QueryException: SQLSTATE[HY000] [1049] Unknown database 'laravel' (SQL: insert into `products` (`updated_at`, `created_at`) values (2022-10-20 12:42:24, 2022-10-20 12:42:24))
Caused by
PDOException: SQLSTATE[HY000] [1049] Unknown database 'laravel'
განვაახლოთ .env ფაილი :
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=vinyl
DB_USERNAME=root
DB_PASSWORD=
ასევე უნდა შევქმნათ მონაცემთა ბაზა სახელწოდებით vinyl. ტესტის გაშვების შემდეგ ახლა უკვე ასეთ შეტყობინებას ვიხილავთ :
1) Tests\Feature\SearchTest::vinyl_search_page_has_all_the_required_page_data
Illuminate\Database\QueryException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'vinyl.products' doesn't exist (SQL: insert into `products` (`updated_at`, `created_at`) values (2022-10-20 12:51:42, 2022-10-20 12:51:42))
Caused by
PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'vinyl.products' doesn't exist
შევქმნათ მიგრაცია :
php artisan make:migration create_products_table
// database/migrations/{datetime}_create_products_table.php
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->float('cost');
$table->string('image');
$table->timestamps();
});
}
გავუშვათ მიგრაციები :
php artisan migrate
ახლა ტესტი გამოგვიტანს ასეთ შეტყობინებას :
1) Tests\Feature\SearchTest::vinyl_search_page_has_all_the_required_page_data
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1364 Field 'name' doesn't have a default value (SQL: insert into `products` (`updated_at`, `created_at`) values (2022-10-20 13:03:56, 2022-10-20 13:03:56))
Caused by
PDOException: SQLSTATE[HY000]: General error: 1364 Field 'name' doesn't have a default value
განვაახლოთ მოდელმწარმოებელი ProductFactory :
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\Product;
class ProductFactory extends Factory
{
protected $model = Product::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => 'Scorpions',
'cost' => 2.5,
'image' => 'https://vnadiradze.ge/info/laravel/images/vinyl.png',
];
}
}
ახლა ტესტი მოგცემს ასეთ შედეგს :
1) Tests\Feature\SearchTest::vinyl_search_page_has_all_the_required_page_data
This test did not perform any assertions
OK, but incomplete, skipped, or risky tests!
Tests: 2, Assertions: 1, Risky: 1.
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\Product;
class SearchTest extends TestCase
{
/** @test */
public function vinyl_search_page_is_accessible()
{
$this->get('/')->assertOk();
}
/** @test */
public function vinyl_search_page_has_all_the_required_page_data()
{
// მომზადების ფაზა
Product::factory()->count(3)->create(); // შევქმნათ 3 ვინილი
// ქმედების ფაზა
$response = $this->get('/');
}
}
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\Product;
class SearchTest extends TestCase
{
/** @test */
public function vinyl_search_page_is_accessible()
{
$this->get('/')->assertOk();
}
/** @test */
public function vinyl_search_page_has_all_the_required_page_data()
{
// მომზადების ფაზა
Product::factory()->count(3)->create(); // შევქმნათ 3 ვინილი
// ქმედების ფაზა
$response = $this->get('/');
// მტკიცება
$items = Product::get();
$response->assertViewIs('search')->assertViewHas('items', $items);
}
}
ტესტის შედეგი იქნება :
1) Tests\Feature\SearchTest::vinyl_search_page_has_all_the_required_page_data
Failed asserting that null is an instance of class "Illuminate\Database\Eloquent\Collection".
მარშრუტი გადავაკეთოთ ასე :
// routes/web.php
Route::get('/', function () {
$items = App\Models\Product::get();
return view('search', compact('items'));
});
ახლა უკვე წარმატებით დასრულდება ტესტი.
php artisan make:controller SearchProductsController
SearchProductsController :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Product;
class SearchProductsController extends Controller
{
public function index()
{
$items = Product::get();
return view('search', compact('items'));
}
}
routes/web.php :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\SearchProductsController;
Route::get('/', [SearchProductsController::class, 'index']);
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\Product;
class SearchTest extends TestCase
{
/** @test */
public function vinyl_search_page_is_accessible()
{
$this->get('/')->assertOk();
}
/** @test */
public function vinyl_search_page_has_all_the_required_page_data()
{
// მომზადების ფაზა
Product::factory()->count(3)->create(); // შევქმნათ 3 ვინილი
// ქმედების ფაზა
$response = $this->get('/');
// მტკიცება
$items = Product::get();
$response->assertViewIs('search')->assertViewHas('items', $items);
}
/** @test */
public function vinyl_search_page_shows_the_items()
{
Product::factory()->count(3)->create();
$items = Product::get();
$this->get('/')
->assertSeeInOrder([
$items[0]->name,
$items[1]->name,
$items[2]->name,
]);
}
}
ტესტის შედეგი იქნება ამდაგვარი :
1) Tests\Feature\SearchTest::vinyl_search_page_shows_the_items
Failed asserting that Failed asserting that '' contains "Scorpions" in specified order..
ყურადღება მივაქციოთ შეტყობინების ამ ფრაგმენტს : Failed asserting that Failed asserting that '' contains "Scorpions" in
specified order..
საქმე იმაშია, რომ assertSeeInOrder მეთოდმა წაიკითხა search.blade.php ფაილის შიგთავსი, დააფორმატა იგი სტრიქონად, შემდეგ კი გადაამოწმა ეს სტრიქონი შეიცავდა თუ არა მოდელმწარმოებლის მიერ შექმნილი ვინილების დასახელებებს (ეს დასახელება ყველასთვის ერთია ამჟამად - 'Scorpions'). წარმოდგენის ფაილში ამ მომენტისათვის არაფერი გვაქვს, შესაბამისად ტესტიც წარუმატებლად დასრულდა.
search.blade.php ფაილში შევიტანოთ შემდეგი კოდი :
<html lang="en">
<head>
<title>Vinyl</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
</head>
<body>
<main>
<div class="album py-5">
<div class="container">
<div class="row">
@foreach($items as $item)
<div class="col-md-4">
<div class="card mb-4 box-shadow">
<img class="card-img-top" src="{{ $item->image }}">
<div class="card-body">
<p class="card-text">
{{ $item->name }}
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary">+</button>
</div>
<small class="text-muted">{{ $item->cost }} ₾</small>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</main>
</body>
</html>
ტესტი ახლა უკვე წარმატებით შესრულდება.
ერთადერთი რაც ამ გვერდზე დაგვრჩა არის ძებნის ფუნქციონალი და მისი ტესტი. ჩავამატოთ ახალი ტესტი:
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\Product;
class SearchTest extends TestCase
{
/** @test */
public function vinyl_search_page_is_accessible()
{
$this->get('/')->assertOk();
}
/** @test */
public function vinyl_search_page_has_all_the_required_page_data()
{
// მომზადების ფაზა
Product::factory()->count(3)->create(); // შევქმნათ 3 ვინილი
// ქმედების ფაზა
$response = $this->get('/');
// მტკიცება
$items = Product::get();
$response->assertViewIs('search')->assertViewHas('items', $items);
}
/** @test */
public function vinyl_search_page_shows_the_items()
{
Product::factory()->count(3)->create();
$items = Product::get();
$this->get('/')
->assertSeeInOrder([
$items[0]->name,
$items[1]->name,
$items[2]->name,
]);
}
/** @test */
public function vinyl_can_be_searched_given_a_query()
{
/*
* შევქმნათ სამი სხვადასხვა დასახელების მქონე ვინილი
*/
Product::factory()->create([
'name' => 'Metallica'
]);
Product::factory()->create([
'name' => 'Guns N roses'
]);
Product::factory()->create([
'name' => 'Pink floyd'
]);
// მოვძებნოთ ერთ-ერთი მათგანის დასახელებით
$this->get('/?query=metallica')
->assertSee('Metallica') // ეს უნდა დავინახოთ პასუხში
->assertDontSeeText('Guns N roses') // ეს ვერ უნდა დავინახოთ პასუხში
->assertDontSeeText('Pink floyd'); // ეს ვერ უნდა დავინახოთ პასუხში
// ფილტრის გარეშე სამივე მათგანი უნდა დავინახოთ
$this->get('/')->assertSeeInOrder([
'Metallica',
'Guns N roses',
'Pink floyd'
]);
}
}
SearchProductsController :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Product;
class SearchProductsController extends Controller
{
public function index()
{
$query_str = request('query');
$items = Product::when($query_str, function ($query, $query_str) {
return $query->where('name', 'LIKE', "%{$query_str}%");
})->get();
return view('search', compact('items', 'query_str'));
}
}
search.blade.php ფაილში კი ჩავამატოთ საძიებო ფორმა :
<html lang="en">
<head>
<title>Vinyl</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
</head>
<body>
<main>
<div class="album py-5">
<div class="container">
<div class="row">
<div class="col-md-12">
<form action="/" method="GET">
<input class="form-control form-control-lg" type="query" name="query" value="{{ $query_str ?? '' }}" placeholder="რისი მოსმენა გსურთ ?">
</form>
</div>
</div>
<div class="row">
@foreach($items as $item)
<div class="col-md-4">
<div class="card mb-4 box-shadow">
<img class="card-img-top" src="{{ $item->image }}">
<div class="card-body">
<p class="card-text">
{{ $item->name }}
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary">+</button>
</div>
<small class="text-muted">{{ $item->cost }} ₾</small>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</main>
</body>
</html>
აღსანიშნავია, რომ ამ მომენტამდე რეალურ მონაცემთა ბაზას ვიყენებდით. products ცხრილს თუ გადავამოწმეთ შევამჩნევთ საკმაოდ ბევრ ჩანაწერს, რომლებიც მოდელმწარმოებლების მიერ შეიქმნებოდა ტესტირების დროს. თუ არ გვსურს ბაზა გადაიტვირთოს ბევრის სატესტო ინფორმაციით, უნდა გამოვიყენოთ RefreshDatabase ტრეიტი. იგი ასრულებს migrate:fresh ბრძანებას ტესტების ყოველი გაშვების შემდეგ. ხოლო თუ საერთოდ არ გვსურს რეალური ბაზის გამოყენება, მაშინ უნდა განვაკომენტაროთ phpunit.xml ფაილში არსებული ეს კონფიგურაციული პარამეტრები :
php artisan make:test CartTest
კალათის ტესტში, პირველ რიგში გადავამოწმოთ, გვაქვს თუ არა შესაბამისი მარშრუტი, შემდეგ გავაგზავნოთ მოთხოვნა ამ მარშრუტზე და ბოლოს კი გადავამოწმოთ
განახლდა თუ არა სესია, CartTest :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\Product;
class CartTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function item_can_be_added_to_the_cart()
{
$this->post('/add_to_cart', [
'id' => 1,
])
->assertRedirect('/')
->assertSessionHasNoErrors()
->assertSessionHas('cart.0', [
'id' => 1,
'qty' => 1,
]);
}
}
/cart წერტილზე გავაგზავნეთ POST ტიპის მოთხოვნა. მასივში, რომელიც მეორე პარამეტრად გადავეცით post მეთოდს,
აღწერილია ის ინფორმაცია, რომელიც ბრაუზერიდან გაიგზავნება პროდუქტის კალათში დამატების დროს (პროდუქტის id). ამის შემდეგ კი გვაქვს სამი სხვადასხვა მტკიცება :
use App\Http\Controllers\CartController;
Route::post('/add_to_cart', [CartController::class, 'store']);
შევქმნათ კონტროლერიც :
php artisan make:controller CartController
CartController :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Product;
class CartController extends Controller
{
public function store()
{
$existing = collect(session('cart'))->first(function ($row, $key) {
return $row['id'] == request('id');
});
if (!$existing) {
session()->push('cart', [
'id' => request('id'),
'qty' => 1,
]);
}
return redirect('/');
}
}
ტესტი წარმატებით გაეშვება, მაგრამ ახლა პრობლემა ისაა, რომ წარმოდგენის ფაილში არ გვაქვს კალათში დამატების ფორმა,
search.blade.php :
<html lang="en">
<head>
<title>Vinyl</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
</head>
<body>
<main>
<div class="album py-5">
<div class="container">
<div class="row">
<div class="col-md-12">
<form action="/" method="GET">
<input class="form-control form-control-lg" type="query" name="query" value="{{ $query_str ?? '' }}" placeholder="რისი მოსმენა გსურთ ?">
</form>
</div>
</div>
<div class="row">
@foreach($items as $item)
<div class="col-md-4">
<div class="card mb-4 box-shadow">
<img class="card-img-top" src="{{ $item->image }}">
<div class="card-body">
<p class="card-text">
{{ $item->name }}
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<form action="/add_to_cart" method="post">
@csrf
<input type="hidden" name="id" value="{{ $item->id }}">
<button type="submit" class="btn btn-sm btn-outline-primary">დამატება</button>
</form>
</div>
<small class="text-muted">{{ $item->cost }} ₾</small>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</main>
</body>
</html>
ახლა მივხედოთ უშუალოდ კალათის გვერდს, CartTest.php :
/** @test */
public function cart_page_can_be_accessed()
{
$this->get('/cart')->assertViewIs('cart');
}
შევქმნათ მარშრუტი, routes/web.php :
Route::get('/cart', [CartController::class, 'index']);
შევქმნათ წარმოდგენის ფაილი cart.blade.php. კონტროლერში კი ჩავამატოთ შესაბამისი მეთოდი, CartController.php :
public function index()
{
$items = Product::whereIn('id', collect(session('cart'))->pluck('id'))->get();
$cart_items = collect(session('cart'))->map(function ($row, $index) use ($items) {
return [
'id' => $row['id'],
'qty' => $row['qty'],
'name' => $items[$index]->name,
'image' => $items[$index]->image,
'cost' => $items[$index]->cost,
];
})->toArray();
return view('cart', compact('cart_items'));
}
ახლა ტესტის მეშვეობით გადავამოწმოთ ჩანს თუ არა კალათში დამატებული ვინილები კალათის გვერდზე. ამისათვის გამოვიყენოთ assertSeeTextInOrder()
მტკიცებითი მეთოდი, რომელიც გადაამოწმებს ემთხვევა თუ არა წარმოდგენის ფაილის მიხედვით დაგენერირებულ სტრიქონში არსებული ინფორმაცია, მეთოდისათვის პარამეტრად
გადაცემული მასივის ელემენტებს თანმიმდევრობას :
CartTest.php :
/** @test */
public function items_added_to_the_cart_can_be_seen_in_the_cart_page()
{
Product::factory()->create([
'name' => 'Scorpions',
'cost' => 1.5,
]);
Product::factory()->create([
'name' => 'Metallica',
'cost' => 2.1,
]);
Product::factory()->create([
'name' => 'The Beatles',
'cost' => 3.2,
]);
$this->post('/add_to_cart', [
'id' => 1, // Scorpions
]);
$this->post('/add_to_cart', [
'id' => 3, // The Beatles
]);
$cart_items = [
[
'id' => 1,
'qty' => 1,
'name' => 'Scorpions',
'image' => 'https://vnadiradze.ge/info/laravel/images/vinyl.png',
'cost' => 1.5,
],
[
'id' => 3,
'qty' => 1,
'name' => 'The Beatles',
'image' => 'https://vnadiradze.ge/info/laravel/images/vinyl.png',
'cost' => 3.2,
],
];
$this->get('/cart')
->assertViewHas('cart_items', $cart_items)
->assertSeeTextInOrder([
'Scorpions',
'The Beatles',
])
->assertDontSeeText('Metallica');
}
ბუნებრივია ტესტი წარუმატებელი იქნება რადგან cart.blade.php ფაილი ცარიელია, გამოვიტანოთ მასში კალათის შიგთავსი :
<html lang="en">
<head>
<title>Vinyl</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
</head>
<body>
<main>
<div class="album py-5">
<div class="container">
<div class="row">
@foreach($cart_items as $item)
<div class="col-md-4">
<div class="card mb-4 box-shadow">
<img class="card-img-top" src="{{ $item['image'] }}">
<div class="card-body">
<p class="card-text">
{{ $item['name'] }}
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<input type="number" value="{{ $item['qty'] }}" style="width: 50px;" min="1" class="mr-2">
<button type="button" class="btn btn-sm btn-outline-success mr-2">განახლება</button>
<button type="submit" class="btn btn-sm btn-outline-danger">წაშლა</button>
</div>
<small class="text-muted">{{ $item['cost'] }} ₾</small>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</main>
</body>
</html>
ახლა შევქმნათ კალათიდან ვინილის წაშლის ფუნქციონალი, რა თქმა უნდა დავიწყოთ ტესტით, CartTest.php ფაილში ჩავამატოთ ახალი ტესტი:
/** @test */
public function item_can_be_removed_from_the_cart()
{
Product::factory()->create([
'name' => 'Scorpions',
'cost' => 1.5,
]);
Product::factory()->create([
'name' => 'Metallica',
'cost' => 2.1,
]);
Product::factory()->create([
'name' => 'The Beatles',
'cost' => 3.2,
]);
// აქვე დავამატოთ ვინილები სესიაში
session(['cart' => [
['id' => 2, 'qty' => 1], // Metallica
['id' => 3, 'qty' => 3], // The Beatles
]]);
$this->delete('/cart/2') // წავშალოთ Metallica
->assertRedirect('/cart')
->assertSessionHasNoErrors()
->assertSessionHas('cart', [
['id' => 3, 'qty' => 3]
]);
// გადავამოწმოთ გამოდის თუ არა კალათის გვერდზე მოსალოდნელი ინფორმაცია
$this->get('/cart')
->assertSeeInOrder([
'The Beatles', // ვინილის დასახელება
'3', // რაოდენობა
'3.2 ₾', // ფასი
])
->assertDontSeeText('Metallica');
}
ასევე შევქმნათ მარშრუტი, routes/web.php :
Route::delete('/cart/{id}', [CartController::class, 'destroy']);
კონტროლერშიც ჩავამატოთ შესაბამისი მეთოდი, CartController.php :
public function destroy()
{
$id = request('id');
$items = collect(session('cart'))->filter(function ($item) use ($id) {
return $item['id'] != $id;
})->values()->toArray();
session(['cart' => $items]);
return redirect('/cart');
}
წაშლის ფუნქციონალი ჩვამატოთ წარმოდგენის ფაილშიც, cart.blade.php :
<html lang="en">
<head>
<title>Vinyl</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
</head>
<body>
<main>
<div class="album py-5">
<div class="container">
<div class="row">
@foreach($cart_items as $item)
<div class="col-md-4">
<div class="card mb-4 box-shadow">
<img class="card-img-top" src="{{ $item['image'] }}">
<div class="card-body">
<p class="card-text">
{{ $item['name'] }}
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<input type="number" value="{{ $item['qty'] }}" style="width: 50px;" min="1" class="mr-2">
<button type="button" class="btn btn-sm btn-outline-success mr-2">განახლება</button>
<form action="/cart/{{ $item['id'] }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger">წაშლა</button>
</form>
</div>
<small class="text-muted">{{ $item['cost'] }} ₾</small>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</main>
</body>
</html>
ახლა შევქმნათ კალათში რაოდენოვის განახლების ფუნქციონალი, ტრადიციულად დავიწყოთ ტესტით, CartTest.php ფაილში ჩავამატოთ ახალი ტესტი:
/** @test */
public function cart_item_qty_can_be_updated()
{
Product::factory()->create([
'name' => 'Scorpions',
'cost' => 1.5,
]);
Product::factory()->create([
'name' => 'Metallica',
'cost' => 2.1,
]);
Product::factory()->create([
'name' => 'The Beatles',
'cost' => 3.2,
]);
// დავამატოთ პრდუქტი კალათში
session(['cart' => [
['id' => 1, 'qty' => 1], // Scorpions
['id' => 3, 'qty' => 1], // The Beatles
]]);
$this->patch('/cart/3', [ // განვაახლოთ The Beatles-ის რაოდენობა
'qty' => 5,
])
->assertRedirect('/cart')
->assertSessionHasNoErrors()
->assertSessionHas('cart', [
['id' => 1, 'qty' => 1],
['id' => 3, 'qty' => 5],
]);
// დავადასტუროთ, რომ კალათის გვერდზე სწორი ინფორმაცია ჩანს
$this->get('/cart')
->assertSeeInOrder([
'Scorpions',
'1', // რაოდენობა
'1.5 ₾', // ფასი
'The Beatles',
'5', // რაოდენობა
'3.2 ₾', // ფასი
]);
}
ასევე შევქმნათ მარშრუტი, routes/web.php :
Route::patch('/cart/{id}', [CartController::class, 'update']);
კონტროლერშიც ჩავამატოთ შესაბამისი მეთოდი, CartController.php :
public function update()
{
$id = request('id');
$qty = request('qty');
$items = collect(session('cart'))->map(function ($row) use ($id, $qty) {
if ($row['id'] == $id) {
return ['id' => $row['id'], 'qty' => $qty];
}
return $row;
})->toArray();
session(['cart' => $items]);
return redirect('/cart');
}
რაოდენობის განახლების ფუნქციონალი ჩვამატოთ წარმოდგენის ფაილშიც, cart.blade.php :
<html lang="en">
<head>
<title>Vinyl</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
</head>
<body>
<main>
<div class="album py-5">
<div class="container">
<div class="row">
@if ($cart_items && count($cart_items) > 0)
@foreach($cart_items as $item)
<div class="col-md-4">
<div class="card mb-4 box-shadow">
<img class="card-img-top" src="{{ $item['image'] }}">
<div class="card-body">
<p class="card-text">
{{ $item['name'] }}
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<form method="POST" action="/cart/{{ $item['id'] }}" class="row" class="mr-2">
@csrf
@method('PATCH')
<input type="number" name="qty" value="{{ $item['qty'] }}" style="width: 50px;" min="1">
<button type="submit" class="btn btn-sm btn-outline-success mr-2">განახლება</button>
</form>
<form action="/cart/{{ $item['id'] }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger">წაშლა</button>
</form>
</div>
<small class="text-muted">{{ $item['cost'] }} ₾</small>
</div>
</div>
</div>
</div>
@endforeach
<div class="d-grid gap-2">
<button class="btn btn-primary" type="button">გადახდა</button>
</div>
@else
<div>კალათა ცარიელია</div>
@endif
</div>
</div>
</div>
</main>
</body>
</html>
php artisan make:test CheckoutTest
პირველ რიგში გადავამოწმოთ ჩანს თუ არა კალათში დამატებული ვინილები შეკვეთის გვერდზე, CheckoutTest.php :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\Product;
class CheckoutTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function cart_items_can_be_seen_from_the_checkout_page()
{
Product::factory()->create([
'name' => 'Scorpions',
'cost' => 1.5,
]);
Product::factory()->create([
'name' => 'Metallica',
'cost' => 2.1,
]);
Product::factory()->create([
'name' => 'The Beatles',
'cost' => 3.2,
]);
session([
'cart' => [
['id' => 2, 'qty' => 1], // Metallica
['id' => 3, 'qty' => 2], // The Beatles
],
]);
$checkout_items = [
[
'id' => 2,
'qty' => 1,
'name' => 'Metallica',
'image' => 'https://vnadiradze.ge/info/laravel/images/vinyl.png',
'cost' => 2.1,
'subtotal' => 2.1
],
[
'id' => 3,
'qty' => 2,
'name' => 'The Beatles',
'image' => 'https://vnadiradze.ge/info/laravel/images/vinyl.png',
'cost' => 3.2,
'subtotal' => 6.4
],
];
$this->get('/checkout')
->assertViewIs('checkout')
->assertViewHas('checkout_items', $checkout_items)
->assertSeeTextInOrder([
'Metallica',
'2.1 ₾',
'1x',
'2.1 ₾',
'The Beatles',
'3.2 ₾',
'2x',
'6.4 ₾',
'8.5 ₾', // ჯამი
]);
}
}
შევქმნათ კონტროლერიც :
php artisan make:controller CheckoutController
კონტროლერში შევიტანოთ შემდეგი კოდი :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Product;
class CheckoutController extends Controller
{
public function index()
{
$items = Product::whereIn('id', collect(session('cart'))->pluck('id'))->get();
$checkout_items = collect(session('cart'))->map(function ($row,$index) use ($items) {
$qty = (int) $row['qty'];
$cost = (float) $items[$index]->cost;
$subtotal = $cost * $qty;
return [
'id' => $row['id'],
'qty' => $qty,
'name' => $items[$index]->name,
'image' => $items[$index]->image,
'cost' => $cost,
'subtotal' => round($subtotal, 2),
];
});
$total = $checkout_items->sum('subtotal');
$checkout_items = $checkout_items->toArray();
return view('checkout', compact('checkout_items', 'total'));
}
}
არ დაგვავიწყდეს მარშრუტის ფაილის განახლებაც, routes/web.php :
use App\Http\Controllers\CheckoutController;
Route::get('/checkout', [CheckoutController::class, 'index']);
შევქმნათ წარმოდგენის ფაილიც, checkout.blade.php :
<html lang="en">
<head>
<title>Vinyl</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
</head>
<body>
<main>
<div class="album py-5">
<div class="container">
<div class="row">
<h3>შეკვეთის დეტალები</h3>
<table class="table mt-4">
<thead>
<tr>
<th scope="col">ვინილი</th>
<th scope="col">ფასი</th>
<th scope="col">რაოდენობა</th>
<th scope="col">ჯამი</th>
</tr>
</thead>
<tbody>
@foreach ($checkout_items as $item)
<tr>
<td>{{ $item['name'] }}</td>
<td>{{ $item['cost'] }} ₾</td>
<td>{{ $item['qty'] }}x</td>
<td>{{ $item['subtotal'] }} ₾</td>
</tr>
@endforeach
<tr>
<td colspan="3">მთლიანი ჯამი {{ $total }} ₾</td>
<td class="justify-content-end">
<a href="" class="btn btn-primary">შეკვეთა</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</body>
</html>
ახლა შევქმნათ უშუალოდ შეკვეთის გაკეთების ფუნქციონალი. შესაბამის მარშრუტზე მიკითხვისას ბაზაში უნდა შევინახოთ ახალი ჩანაწერი შეკვეთის შესახებ.
გამოვიყენოთ assertDatabaseHas() მეთოდი, რომელიც გადაამოწმებს ნამდვილად შეიქმნა თუ არა ჩანაწერი ბაზაში. CheckoutTest.php
ტესტში ჩავამატოთ ახალი მეთოდი :
/** @test */
public function order_can_be_created()
{
Product::factory()->create([
'name' => 'Scorpions',
'cost' => 1.5,
]);
session([
'cart' => [
['id' => 1, 'qty' => 2]
],
]);
$this->post('/checkout')->assertSessionHasNoErrors();
// გადავამოწმოთ ნამდვილად შეიქმნა თუ არა ჩანაწრი ბაზაში
$this->assertDatabaseHas('orders', [
'total' => 3, // 2 x 1.5
]);
$this->assertDatabaseHas('order_details', [
'order_id' => 1,
'product_id' => 1,
'cost' => 1.5,
'qty' => 2,
]);
}
დაგვჭირდება ორი ახალი ცხრილის შექმნაც :
php artisan make:migration create_orders_table
php artisan make:migration create_order_details_table
მიგრაციის ფაილებში აღვწეროთ საჭირო ველები, database/migrations/{datetime}_create_orders_table.php :
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->float('total');
$table->timestamps();
});
}
database/migrations/{datetime}_create_order_details_table.php :
public function up()
{
Schema::create('order_details', function (Blueprint $table) {
$table->id();
$table->bigInteger('order_id');
$table->bigInteger('product_id');
$table->float('cost');
$table->integer('qty');
});
}
გავუშვათ მიგრაციები :
php artisan migrate:fresh
ახლა შევქმნათ მოდელები :
php artisan make:model Order
php artisan make:model OrderDetail
app/Models/Order.php :
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\OrderDetail;
class Order extends Model
{
use HasFactory;
protected $guarded = []; // არ არის რეკომენდებული, ვწერთ მხოლოდ სატესტოდ
public function detail()
{
return $this->hasMany(OrderDetail::class, 'order_id');
}
}
app/Models/OrderDetail.php :
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Order;
class OrderDetail extends Model
{
use HasFactory;
protected $guarded = []; // არ არის რეკომენდებული, ვწერთ მხოლოდ სატესტოდ
public $timestamps = false;
}
CheckoutController კონტროლერში ჩავამატოთ ახალი მეთოდი - create და ასევე შევაიმპორტოთ Order მოდელი :
...
use App\Models\Order;
...
public function create()
{
$items = Product::whereIn('id', collect(session('cart'))->pluck('id'))->get();
$checkout_items = collect(session('cart'))->map(function ($row, $index) use ($items) {
$qty = $row['qty'];
$cost = $items[$index]->cost;
$subtotal = $cost * $qty;
return [
'id' => $row['id'],
'qty' => $qty,
'name' => $items[$index]->name,
'cost' => $cost,
'subtotal' => $subtotal
];
});
$total = $checkout_items->sum('subtotal');
$order = Order::create([
'total' => $total,
]);
foreach ($checkout_items as $item) {
$order->detail()->create([
'product_id' => $item['id'],
'cost' => $item['cost'],
'qty' => $item['qty'],
]);
}
return redirect('/');
}
დავამატოთ მარშრუტიც, routes/web.php :
Route::post('/checkout', [CheckoutController::class, 'create']);
დაბოლოს, წარმოდგენის ფაილში ჩავამატოთ შეკვეთის გასაგზავნი ფორმა, checkout.blade.php :
<html lang="en">
<head>
<title>Vinyl</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
</head>
<body>
<main>
<div class="album py-5">
<div class="container">
<div class="row">
<h3>შეკვეთის დეტალები</h3>
<table class="table mt-4">
<thead>
<tr>
<th scope="col">ვინილი</th>
<th scope="col">ფასი</th>
<th scope="col">რაოდენობა</th>
<th scope="col">ჯამი</th>
</tr>
</thead>
<tbody>
@foreach ($checkout_items as $item)
<tr>
<td>{{ $item['name'] }}</td>
<td>{{ $item['cost'] }} ₾</td>
<td>{{ $item['qty'] }}x</td>
<td>{{ $item['subtotal'] }} ₾</td>
</tr>
@endforeach
<tr>
<td colspan="3">მთლიანი ჯამი {{ $total }} ₾</td>
<td class="justify-content-end">
<form method="post" action="/checkout">
@csrf
<button type="submit" class="btn btn-primary">შეკვეთა</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</body>
</html>
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Product;
class SearchProductsController extends Controller
{
public function index()
{
$query_str = request('query');
$items = Product::when($query_str, function ($query, $query_str) {
return $query->where('name', 'LIKE', "%{$query_str}%");
})->get();
return view('search', compact('items', 'query_str'));
}
}
მოვახდინოთ ძებნის ლოგიკის ინკაფსულაცია მოდელში, app/Models/Product.php :
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
protected $guarded = []; // არ არის რეკომენდებული, ვწერთ მხოლოდ სატესტოდ
public static function matches($query_str)
{
return self::when($query_str, function ($query, $query_str) {
return $query->where('name', 'LIKE', "%{$query_str}%");
});
}
}
კონტროლერი კი გადავაკეთოთ შემდეგნაირად :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Product;
class SearchProductsController extends Controller
{
public function index()
{
$query_str = request('query');
$items = Product::matches($query_str)->get(); // update this
return view('search', compact('items'));
}
}
namespace App\Services;
use App\Models\Product;
class CartService
{
private $cart;
private $items;
public function __construct()
{
$this->cart = collect(session('cart'));
$this->items = Product::whereIn('id', $this->cart->pluck('id'))->get();
}
public function get()
{
return $this->cart->map(function ($row, $index) {
return [
'id' => $row['id'],
'qty' => $row['qty'],
'name' => $this->items[$index]->name,
'image' => $this->items[$index]->image,
'cost' => $this->items[$index]->cost,
];
})
->toArray();
}
private function exists($id)
{
return $this->cart->first(function ($row, $key) use ($id) {
return $row['id'] == $id;
});
}
public function add($id)
{
$existing = $this->exists($id);
if (!$existing)
{
session()->push('cart', [
'id' => $id,
'qty' => 1,
]);
return true;
}
return false;
}
public function remove($id)
{
$items = $this->cart->filter(function ($item) use ($id) {
return $item['id'] != $id;
})
->values()
->toArray();
session(['cart' => $items]);
}
public function update($id, $qty)
{
$items = $this->cart->map(function ($row) use ($id, $qty) {
if ($row['id'] == $id) {
return ['id' => $row['id'], 'qty' => $qty];
}
return $row;
})
->toArray();
session(['cart' => $items]);
}
}
კონტროლერს კი მივცეთ ასეთი სახე :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Product;
use App\Services\CartService;
class CartController extends Controller
{
public function index(CartService $cart)
{
$cart_items = $cart->get();
return view('cart', compact('cart_items'));
}
public function store(CartService $cart)
{
$cart->add(request('id'));
return redirect('/');
}
public function destroy(CartService $cart)
{
$id = request('id');
$cart->remove($id);
return redirect('/cart');
}
public function update(CartService $cart)
{
$cart->update(request('id'), request('qty'));
return redirect('/cart');
}
}
public function get()
{
return $this->cart->map(function ($row, $index) {
$qty = (int) $row['qty'];
$cost = (float) $this->items[$index]->cost;
$subtotal = $cost * $qty;
return [
'id' => $row['id'],
'qty' => $qty,
'name' => $this->items[$index]->name,
'image' => $this->items[$index]->image,
'cost' => $cost,
'subtotal' => round($subtotal, 2)
];
})->toArray();
}
სერვისში ასევე დაგვჭირდება კალათის მთლიანი ჯამური თანხის დასათვლელი ახალი მეთოდი - total :
public function total()
{
$items = collect($this->get());
return $items->sum('subtotal');
}
ახლა გადავაკეთოთ კონტროლერიც, CheckoutController :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Product;
use App\Models\Order;
use App\Services\CartService;
class CheckoutController extends Controller
{
public function index(CartService $cart)
{
$checkout_items = $cart->get();
$total = $cart->total();
return view('checkout', compact('checkout_items', 'total'));
}
public function create(CartService $cart)
{
$checkout_items = $cart->get();
$total = $cart->total();
$order = Order::create([
'total' => $total,
]);
foreach ($checkout_items as $item) {
$order->detail()->create([
'product_id' => $item['id'],
'cost' => $item['cost'],
'qty' => $item['qty'],
]);
}
return redirect('/');
}
}
შევნიშნოთ რომ ტესტის გაშვების შემდეგ, წარუმატებლად დასრულდება items_added_to_the_cart_can_be_seen_in_the_cart_page ტესტი,
ეს იმიტომ, რომ subtotal ველის დამატების შემდეგ, წარმოდგენის ფაილისათვის გადასაცემი ინფორმაციების თანმიმდევრობაც შეიცვლებოდა, ამიტომ
ამ ტესტში ჩავამატოთ ეს ახალი ინფორმაციაც :
/** @test */
public function items_added_to_the_cart_can_be_seen_in_the_cart_page()
{
$this->withoutExceptionHandling();
Product::factory()->create([
'name' => 'Scorpions',
'cost' => 1.5,
]);
Product::factory()->create([
'name' => 'Metallica',
'cost' => 2.1,
]);
Product::factory()->create([
'name' => 'The Beatles',
'cost' => 3.2,
]);
$this->post('/add_to_cart', [
'id' => 1, // Scorpions
]);
$this->post('/add_to_cart', [
'id' => 3, // The Beatles
]);
$cart_items = [
[
'id' => 1,
'qty' => 1,
'name' => 'Scorpions',
'image' => 'https://vnadiradze.ge/info/laravel/images/vinyl.png',
'cost' => 1.5,
'subtotal' => 1.5
],
[
'id' => 3,
'qty' => 1,
'name' => 'The Beatles',
'image' => 'https://vnadiradze.ge/info/laravel/images/vinyl.png',
'cost' => 3.2,
'subtotal' => 3.2
],
];
$this->get('/cart')
->assertViewHas('cart_items', $cart_items)
->assertSeeTextInOrder([
'Scorpions',
'The Beatles',
])
->assertDontSeeText('Metallica');
}
საბოლოოდ გავუშვათ ტესტები და დავრწმუნდეთ, რომ კოდის დახვეწის შემდეგ არაფერი გაგვიფუჭებია :)))
ტესტირებაზე ორიენტირებული ჩვენი პირველი პროექტი წარმატებით დასრულდა !!!
git clone https://github.com/VasilNadiradze/vinyl-order-laravel-tdd-mvc-app.git
cd vinyl-order-laravel-tdd-mvc-app
შემდეგ დააინსტალირეთ კომპოზერის დამოკიდებულებები :
composer install
დააკოპირეთ .env.example ფაილი, ასლს დაარქვით .env და მასში აღწერეთ მონაცემთა ბაზის, თქვენთვის სასურველი პარამეტრები.
დააგენერირეთ აპლიკაციის გასაღები :
php artisan key:generate
გაუშვით პროექტი :
php artisan serve
composer create-project laravel/laravel todo-api
cd todo-api
php artisan serve
მოვხსნათ კომენტარები phpunit.xml ფაილში არსებულ ამ კონფიგურაციულ პარამეტრებს :
როგორც ვიცით, API-ს შექმნისას მარშრუტებთან სამუშაოდ გამოიყენება routes/api.php ფაილი, ყველაფერი დავიწყოთ სუფთა ფურცლიდან :
<?php
use Illuminate\Support\Facades\Route;
// აქ დავწერთ მარშრუტებს ^_^
php artisan make:test TodoListTest
ამ ბრძანებით შექმნილი ნებისმიერი ტესტი არის tests/TestCase.php ფაილში აღწერილი აბსტრაქტული კლასის - TestCase-ს მემკვიდრე, რომელშიც
მხოლოდ CreatesApplication ტრეიტის გამოძახება ხდება, ეს ტრეიტი კი ლარაველის აპლიკაციის ჩატვირთვას ახდენს ტესტების გაშვებამდე.
TodoListTest ტესტში შევიტანოთ ასეთი კოდი :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class TodoListTest extends TestCase
{
public function test_example()
{
// მომზადება
// ქმედება : GET ტიპის, content-type = application/json მოთხოვნა /todo-list ბმულზე
$response = $this->getJson('todo-list');
// მტკიცება : მასივად დაფორმატებული პასუხი შეიცავს 1 ელემენტს
$this->assertEquals(1, count($response->json()));
}
}
გავუშვათ ტესტი :
php artisan test
შედეგი იქნება ამდაგვარი :
სისტემა გვეუბნება რომ ვერ მოხერხდა 1=5 ტოლობის დამტკიცება, ეს ბუნებრივიცაა :)), მაგრამ საიდან მოვიდა ციფრი 5 ? ან რა მოხდა რელურად ? უბრალოდ დავბეჭდოთ მოთხოვნის შედეგად მიღებული პასუხი :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class TodoListTest extends TestCase
{
public function test_example()
{
// მომზადება
// ქმედება
$response = $this->getJson('/todo-list');
dd($response->json());
// მტკიცება
$this->assertEquals(1, count($response->json()));
}
}
ტესტის გაშვების შემდეგ ვიხილავთ ასეთ სურათს :
ანუ შეიძლება ითქვას, რომ სისტემამ მასივის სახით დაგვიბრუნდა ის ყველაფერი რაც ტესტის გაშვების შემდეგ მოხდა, მასივში კი 5 ელემენტია. როგორ ვნახოთ სრული ინფორმაცია მიმდინარე პროცესის შესახებ ? ამისათვის გამოვიყენოთ withoutExceptionHandling მეთოდი :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class TodoListTest extends TestCase
{
public function test_example()
{
$this->withoutExceptionHandling();
// მომზადება
// ქმედება
$response = $this->getJson('/todo-list');
// მტკიცება
$this->assertEquals(1, count($response->json()));
}
}
ამჯერად სისტემა უფრო მეტ ინფორმაციას გამოგვიტანს კულისებს მიღმა მიმდინარე პროცესებზე :
use Illuminate\Support\Facades\Route;
Route::get('todo-list', [TodoListController::class, 'index']);
ტესტის გაშვების შემდეგ ვნახავთ, რომ შედეგი იგივე იქნება რაც წინა ტესტზე, თუმცა ინტუიციურად ალბათ ვხვდებით, რომ ამჯერად სისტემას კონტროლერთან
დაკავშირებული შეცდომა უნდა დაეფიქსირებინა და ეთქვა, რომ ვერ ვიპოვე მითითებული კონტროლერიო. რაშია საქმე ? საქმე იმაშია, რომ სისტემა არც მისულა
ჩვენს მარშრუტამდე. დავაკვირდეთ app/Providers/RouteServiceProvider.php ფაილის შემდეგ ფრაგმენტს :
...
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
...
ეს იმას ნიშნავს, რომ API-სთან მუშაობისას ბმულების პრეფიქსი უნდა იყოს api :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class TodoListTest extends TestCase
{
public function test_example()
{
$this->withoutExceptionHandling();
// მომზადება
// ქმედება
$response = $this->getJson('api/todo-list');
// მტკიცება
$this->assertEquals(1, count($response->json()));
}
}
ახლა უკვე ვიხილავთ მოსალოდნენ შეტყობინებას კონტროლერის შესახებ. შევიტანოთ მცირე ცვლილებები კოდში, კერძოდ : მარშრუტს
დავარქვათ სახელი, routes/api.php :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
Route::get('todo-list', [TodoListController::class, 'index'])->name('todo-list.index');
ტესტში კი ბმულის ნაცვლად მივმართოთ სახელდებულ მარშრუტს :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class TodoListTest extends TestCase
{
public function test_example()
{
$this->withoutExceptionHandling();
// მომზადება
// ქმედება
$response = $this->getJson(route('todo-list.index'));
// მტკიცება
$this->assertEquals(1, count($response->json()));
}
}
ახლა კი შევქმნათ კონტროლერი :
php artisan make:controller Api\TodoListController
TodoListController :
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class TodoListController extends Controller
{
public function index()
{
return response(['lists' => []]); // ამ ეტაპზე დავაბრუნოთ ცარიელი მასივი
}
}
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class TodoListTest extends TestCase
{
public function test_todo_list_index()
{
$this->withoutExceptionHandling();
// მომზადება
// ქმედება
$response = $this->getJson(route('todo-list.index'));
// მტკიცება
$this->assertEquals(1, count($response->json()));
}
}
ვცადოთ ტესტის გაშვება. გილოცავთ ! ეს არის ჩვენი პირველი წარმატებული API ტესტი ! ^_^
სხვათაშორის : თუ შევალთ ამ მისამართზე http://127.0.0.1:8000/api/todo-list, ვიხილავთ ამდაგვარ შედეგს :
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
public function setUp(): void
{
parent::setUp();
$this->withoutExceptionHandling();
}
}
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class TodoListController extends Controller
{
public function index()
{
$lists = TodoList::all();
return response($lists);
}
}
ბუნებრივია ტესტი გვეტყვის, რომ მოდელი არ არსებობს, შევქმნათ იგი :
php artisan make:model TodoList
და ასევე შევაიმპორტოთ კონტროლერში :
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\TodoList;
class TodoListController extends Controller
{
public function index()
{
$lists = TodoList::all();
return response($lists);
}
}
ამჯერად ტესტი გვეტყვის, რომ არ გვაქვს ცხრილი todo_lists. შევქმნათ მიგრაცია :
php artisan make:migration create_todo_lists_table
ასევე არ დაგვავიწყდეს, ჩვენთვის უკვე კარგად ნაცნობი RefreshDatabase ტრეიტის გამოყენება, TodoListTest.php :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class TodoListTest extends TestCase
{
use RefreshDatabase;
public function test_todo_list_index()
{
// მომზადება
// ქმედება
$response = $this->getJson(route('todo-list.index'));
// მტკიცება
$this->assertEquals(1, count($response->json()));
}
}
ამჯერად ტესტი გამოგვიტანს ამდაგვარ შედეგს :
FAIL Tests\Feature\TodoListTest
⨯ todo list index
---
• Tests\Feature\TodoListTest > todo list index
Failed asserting that 0 matches expected 1.
Tests: 1 failed
Time: 0.33s
და ეს ბუნებრივიცაა - ვდილობთ დავამტკიცოთ, რომ ცხრილში არსებული ჩანაწერების რაოდენობა 1-ის ტოლია, ცხრილში კი არაფერი გვაქვს.
სწორედ აქ დაგვჭირდება მომზადების ფაზა, რომელიც აქამდე ცარიელი იყო ტესტში :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\TodoList;
class TodoListTest extends TestCase
{
use RefreshDatabase;
public function test_todo_list_index()
{
// მომზადება
TodoList::create(['name' => 'my list']);
// ქმედება
$response = $this->getJson(route('todo-list.index'));
// მტკიცება
$this->assertEquals(1, count($response->json()));
}
}
app/Models/TodoList.php :
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TodoList extends Model
{
use HasFactory;
protected $guarded = []; // არ არის რეკომენდებული, ვწერთ მხოლოდ სატესტოდ
}
ამჯერად ტესტი გვეტყვის, რომ todo_lists ცხრილში არ გვაქვს name ველი, შევიტანოთ ცვლილებები მიგრაციის ფაილში,
database/migrations/{datetime}_create_todo_lists_table.php :
public function up()
{
Schema::create('todo_lists', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\TodoList;
class TodoListTest extends TestCase
{
use RefreshDatabase;
public function test_todo_list_index()
{
// მომზადება
TodoList::factory()->create();
// ქმედება
$response = $this->getJson(route('todo-list.index'));
// მტკიცება
$this->assertEquals(1, count($response->json()));
}
}
ტესტის გაშვების შემდეგ სისტემა გვეტყვის, რომ ვერ იპოვა TodoListFactory მოდელმწარმოებელი, შევქმნათ იგი :
php artisan make:factory TodoListFactory
database/factories/TodoListFactory.php :
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\TodoList>
*/
class TodoListFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->sentence
];
}
}
თუ მოდელმწარმოებლის გამოყენებისას, create მეთოდს პარამეტრად გადავცემთ რომელიმე ველის მნიშვნელობას, ეს მნიშვნელობა გადაეწერება
თავად მოდელმწარმოებლის definition მეთოდში აღწერილ, ამავე ველის შექმნის ინსტრუქციას, TodoListTest.php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\TodoList;
class TodoListTest extends TestCase
{
use RefreshDatabase;
public function test_todo_list_index()
{
// მომზადება
TodoList::factory()->create(['name' => 'my list']);
// ქმედება
$response = $this->getJson(route('todo-list.index'));
// მტკიცება
$this->assertEquals(1, count($response->json()));
$this->assertEquals('my list', $response->json()[0]['name']);
}
}
public function test_fetch_single_todo_list()
{
// მომზადება
$list = TodoList::factory()->create();
// ქმედება
$response = $this->getJson(route('todo-list.show', $list->id));
// მტკიცება
$response->assertOk();
$this->assertEquals($response->json()['name'], $list->name);
}
ტესტის შედეგად სისტემა მოგვთხოვს მარშრუტს, შევქმნათ იგი, routes/api.php :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
Route::get('todo-list', [TodoListController::class, 'index'])->name('todo-list.index');
Route::get('todo-list/{id}', [TodoListController::class, 'show'])->name('todo-list.show');
ახლა სისტემა მოგვთხოვს კონტროლერის მეთოდს, ჩავამატოთ show მეთოდი :
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\TodoList;
class TodoListController extends Controller
{
public function index()
{
$lists = TodoList::all();
return response($lists);
}
public function show($id)
{
$list = TodoList::find($id);
return response($list);
}
}
ცოტათი დავხვეწოთ test_fetch_single_todo_list ტესტი :
public function test_fetch_single_todo_list()
{
$list = TodoList::factory()->create();
$response = $this->getJson(route('todo-list.show', $list->id))
->assertOk()
->json();
$this->assertEquals($response['name'], $list->name);
}
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
Route::get('todo-list', [TodoListController::class, 'index'])->name('todo-list.index');
Route::get('todo-list/{list}', [TodoListController::class, 'show'])->name('todo-list.show');
კონტროლერის show მეთოდი კი გადავაკეთოთ ასე :
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\TodoList;
class TodoListController extends Controller
{
public function index()
{
$lists = TodoList::all();
return response($lists);
}
public function show(TodoList $list)
{
return response($list);
}
}
ახლა თავად ტესტის კლასსაც მივხედოთ :)) მას თუ დავაკვირდებით, შევამჩნევთ, რომ სიის შექმნის ჩანაწერი რამდენიმეგან მეორდება,
ავირიდოთ თავიდან განმეორებადი კოდი და ეს გავაკეთოთ setUp მეთოდის მეშვეობით, რომელში აღწერილი ინსტრუქციებიც
ყველაზე ადრე სრულდება ტესტის გაშვებისას. უნდა გვახსოვდეს, რომ ამ მეთოდში აუცილებლად უნდა მოვახდინთ მშობელი კლასის,
ამავე სახელწოდების მეთოდის გამოძახებაც, ასევე არ დაგვავიწყდეს ტიპიზაციაც, TodoListTest.php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\TodoList;
class TodoListTest extends TestCase
{
use RefreshDatabase;
private $list;
public function setUp():void
{
parent::setUp();
$this->list = TodoList::factory()->create(['name' => 'my list']);
}
public function test_todo_list_index()
{
// ქმედება
$response = $this->getJson(route('todo-list.index'));
// მტკიცება
$this->assertEquals(1, count($response->json()));
$this->assertEquals('my list', $response->json()[0]['name']);
}
public function test_fetch_single_todo_list()
{
$response = $this->getJson(route('todo-list.show', $this->list->id))
->assertOk()
->json();
$this->assertEquals($response['name'], $this->list->name);
}
}
როგორც ვხედავთ, მომზადების ფაზის აღწერა აღარ გვჭირდება სათითაოდ ყველა ტესტში.
public function test_store_new_todo_list()
{
$this->postJson(route('todo-list.store'),['name' => 'my new list'])
->assertCreated();
}
ტესტი მოგვთხოვს მარშრუტს, ჩავამატოთ იგი, routes/api.php :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
Route::get('todo-list', [TodoListController::class, 'index'])->name('todo-list.index');
Route::get('todo-list/{list}', [TodoListController::class, 'show'])->name('todo-list.show');
Route::post('todo-list', [TodoListController::class, 'store'])->name('todo-list.store');
ამჯერად ტესტი მოგვთხოვს კონტროლერის store მეთოდს, ჩავამატოთ იგი TodoListController კონტროლერში :
public function store(Request $request)
{
return response('', 201);
}
ამჯერად ტესტი წარმატებით დასრულდება, რადგან ჩვენ კონტროლერში სწორედ რესურსის წარმატებით შექმნის სტატუსი დავაბრუნეთ (201),
მაგრამ რეალურად ხომ არაფერი შეგვიქმნია ?! ეს იმას ნიშნავს, რომ ტესტში დამატებითი მტკიცებები გვჭირდება :
public function test_store_new_todo_list()
{
$this->postJson(route('todo-list.store'),['name' => 'my new list'])
->assertCreated();
$this->assertDatabaseHas('todo_lists',['name' => 'my new list']);
}
ამჯერად ტესტი წარუმატებელი იქნება და გვეტყვის, რომ ბაზაში არსებობს ჩანაწერი მაგრამ მისი name ველის მნიშვნელობა არ ემთხვევა ტესტში
მითითებულ მნიშვნელობას. შევიტანოთ ცვლილებები კონტროლერის store მეთოდში :
public function store(Request $request)
{
return TodoList::create($request->all());
}
დავხვეწოთ ტესტის კოდიც, სტატიკური დასახელების ('my new list') ნაცვლად, გამოვიყენოთ მოდელმწარმოებლის make მეთოდის მიერ შექმნილი ჩანაწერის
name ველის მნიშვნელობა'. create მეთოდისაგან განსხვავებით ეს მეთოდი მხოლოდ აგენერირებს ჩანაწერს, ბაზაში არ ინახავს :
public function test_store_new_todo_list()
{
$list = TodoList::factory()->make();
$response = $this->postJson(route('todo-list.store'),['name' => $list->name])
->assertCreated()
->json();
$this->assertEquals($list->name, $response['name']);
$this->assertDatabaseHas('todo_lists',['name' => $list->name]);
}
როგორც ზემოთ დავინახეთ, კონტროლერის store მეთოდში სტატუსს საერთოდ აღარ ვაბრუნებთ, მაგრამ ტესტი მაინც წარმატებით
შესრულდება, არადა რესურსის შექმნის სტატუსის (201) გადამამოწმებელ მტკიცებით მეთოდს (assertCreated) ვიყენებთ ტესტში.
საქმე იმაშია რომ ლარაველმა ავტომატურად შეამჩნია ახლადშექმინლი ჩანაწერი კონტროლერის მეთოდში და პასუხსაც (response) ავტომატურად მიანიჭა სტატუსი 201.
public function test_name_field_validation()
{
/*
* ამ ჩანაწერის გარეშე ლარაველი დაგვიგენერირებს ვალიდაციასთან
* დაკავშირებულ გამონაკლისს და ტესტი არ შესრულდება
*/
$this->withExceptionHandling();
$response = $this->postJson(route('todo-list.store'))
->assertUnprocessable(); // იგივე assertStatus(422)
$response->assertJsonValidationErrors(['name']);
}
კონტროლერის store მეთოდში კი ჩავამატოთ ვალიდაცია :
public function store(Request $request)
{
$request->validate([
'name' => 'required'
]);
return TodoList::create($request->all());
}
public function test_delete_todo_list()
{
// შეხსენება : $this->list აღწერილი გვაქვს TodoListTest კლასის setUp() მეთოდში
$this->deleteJson(route('todo-list.destroy', $this->list))
->assertNoContent(); // იგივე assertStatus(204)
$this->assertDatabaseMissing('todo_lists',['name' => $this->list->name]);
}
ტესტის გაშვების შემდეგ სისტემა მოგვთხოვს წაშლის მარშრუტს, ჩავამატოთ იგი, routes/api.php :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
Route::get('todo-list', [TodoListController::class, 'index'])->name('todo-list.index');
Route::get('todo-list/{list}', [TodoListController::class, 'show'])->name('todo-list.show');
Route::post('todo-list', [TodoListController::class, 'store'])->name('todo-list.store');
Route::delete('todo-list/{list}', [TodoListController::class, 'destroy'])->name('todo-list.destroy');
ამჯერად ტესტი მოგვთხოვს კონტროლერის destroy მეთოდს, ჩავამატოთ იგი TodoListController კონტროლერში :
public function destroy(TodoList $list)
{
$list->delete();
return response('', 204);
}
ამჯერად ტესტი წარმატებით დასრულდება.
public function test_update_todo_list()
{
$this->putJson(route('todo-list.update', $this->list),['name' => 'updated name'])
->assertOk();
$this->assertDatabaseHas('todo_lists',[
'id' => $this->list->id,
'name' => 'updated name'
]);
}
ტესტის გაშვების შემდეგ სისტემა მოგვთხოვს განახლების მარშრუტს, ჩავამატოთ იგი, routes/api.php :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
Route::get('todo-list', [TodoListController::class, 'index'])->name('todo-list.index');
Route::get('todo-list/{list}', [TodoListController::class, 'show'])->name('todo-list.show');
Route::post('todo-list', [TodoListController::class, 'store'])->name('todo-list.store');
Route::delete('todo-list/{list}', [TodoListController::class, 'destroy'])->name('todo-list.destroy');
Route::put('todo-list/{list}', [TodoListController::class, 'update'])->name('todo-list.update');
ამჯერად ტესტი მოგვთხოვს კონტროლერის update მეთოდს, ჩავამატოთ იგი TodoListController კონტროლერში :
public function update(Request $request, TodoList $list)
{
$request->validate([
'name' => 'required'
]);
$list->update($request->all());
return response('', 200);
}
ამჯერად ტესტი წარმატებით დასრულდება.
ჩანაწერის განახლების ვალიდაციის ტესტს ექნება ზუსტად იგივე შინაარსი რაც დამატებისას ჰქონდა.
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
Route::apiResource('todo-list', TodoListController::class);
თუ ახლა ტესტის ბრძანებას გავუშვებთ, ვნახავთ, რომ ის ტესტები, რომლებსაც პარამეტრებად კონკრეტული რესურსები გადაეცემათ, წარუმატებლად დასრულდება.
რატომ ხდება ეს ? გავუშვათ შემდეგი ბრძანება :
php artisan route:list
და დავაკვირდეთ შემდეგ ფრაგმენტებს :
GET|HEAD api/todo-list .............................................................. todo-list.index › Api\TodoListController@index
POST api/todo-list .............................................................. todo-list.store › Api\TodoListController@store
GET|HEAD api/todo-list/{todo_list} .................................................... todo-list.show › Api\TodoListController@show
PUT|PATCH api/todo-list/{todo_list} ................................................ todo-list.update › Api\TodoListController@update
DELETE api/todo-list/{todo_list} .............................................. todo-list.destroy › Api\TodoListController@destroy
apiResource მეთოდმა მარშრუტების არგუმენტები დააგენერირა, მისთვის პირველ პარამეტრად
გადაცემული მიშვნელობის მიხედვით (დაამუშავა 'todo-list' ჩანაწერი და მიიღო 'todo_list'), შესაბამისად სისტემაც ელოდება, რომ ამ მარშრუტების დამამუშავებელ
მეთოდებშიც იგივე დასახელება იქნება გამოყენებული, შევიტანოთ ცვლილებები კონტროლერში :
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\TodoList;
class TodoListController extends Controller
{
public function index()
{
$lists = TodoList::all();
return response($lists);
}
public function show(TodoList $todo_list)
{
return response($todo_list);
}
public function store(Request $request)
{
$request->validate([
'name' => 'required'
]);
return TodoList::create($request->all());
}
public function destroy(TodoList $todo_list)
{
$todo_list->delete();
return response('', 204);
}
public function update(Request $request, TodoList $todo_list)
{
$request->validate([
'name' => 'required'
]);
$todo_list->update($request->all());
return response('', 200);
}
}
php artisan make:request TodoListRequest
ეს ბრძანება შექმნის app/Http/Requests/TodoListRequest.php ფაილს, რომელშიც შევიტანოთ შემდეგი კოდი :
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class TodoListRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required'
];
}
}
TodoListController :
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\TodoList;
use App\Http\Requests\TodoListRequest;
class TodoListController extends Controller
{
public function index()
{
$lists = TodoList::all();
return response($lists);
}
public function show(TodoList $todo_list)
{
return response($todo_list);
}
public function store(TodoListRequest $request)
{
return TodoList::create($request->all());
}
public function destroy(TodoList $todo_list)
{
$todo_list->delete();
return response('', 204);
}
public function update(TodoListRequest $request, TodoList $todo_list)
{
$todo_list->update($request->all());
return response('', 200);
}
}
როგორც ვხედავთ, მას შემდეგ რაც store და update მეთოდებში მოვახდინეთ TodoListRequest კლასის ინექცია,
ამ მეთოდებში ვალიდაციის ინსტრუქციების აღწერა აღარ გვიწევს. ტესტიც კვლავინდებულად წარმატებულად დასრულდება.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_usernme
DB_PASSWORD=your_password
შემდეგ ამოვქოქოთ სერვერი :)) :
php artisan serve
შემდეგ გავუშვათ მიგრაციები :
php artisan migrate
APP_DEBUG=false
ამჯერად შედარებით ლაკონურ და მოკლე შეტყობინებას ვიხილავთ.
php artisan make:test TaskTest
ახლადშექმნილ კლასში შევიტანოთ შემდეგი კოდი :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\Task;
class TaskTest extends TestCase
{
use RefreshDatabase;
public function test_fetch_tasks_for_todo_list()
{
$task = Task::factory()->create();
$response = $this->getJson(route('task.index'))->assertOk()->json();
$this->assertEquals(1, count($response));
$this->assertEquals($task->title, $response[0]['title']);
}
}
ტესტი მოგვთხოვს მოდელს :
php artisan make:Model Task
app/Models/Task.php :
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
use HasFactory;
protected $guarded = []; // არ არის რეკომენდებული, ვწერთ მხოლოდ სატესტოდ
}
ახლა ტესტი მოგვთხოვს მოდელმწარმოებელს, შევქმნათ იგი :
php artisan make:factory TaskFactory
მოდელმწარმოებელში შევიტანოთ შემდეგი კოდი :
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class TaskFactory extends Factory
{
public function definition()
{
return [
'title' => $this->faker->sentence()
];
}
}
ახლა ტესტი გვეტყვის, რომ არ არსებობს tasks ცხრილი, შევქმნათ მიგრაცია :
php artisan make:migration create_tasks_table
database/migrations/{datetime}_create_tasks_table.php :
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->timestamps();
});
}
ამჯერად ტესტი task.index მარშრუტს მოგვთხოვს, routes/api.php ფაილში ჩავამატოთ შემდეგი ჩანაწერები :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
use App\Http\Controllers\Api\TaskController;
Route::apiResource('todo-list', TodoListController::class);
Route::get('task', [TaskController::class,'index'])->name('task.index');
შევქმნათ კონტროლერიც :
php artisan make:controller Api\TaskController
TaskController.php :
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Task;
class TaskController extends Controller
{
public function index()
{
$tasks = Task::all();
return response($tasks);
}
}
ამჯერად ტესტი წარმატებით დასრულდება.
public function test_store_task_for_todo_list()
{
$task = Task::factory()->make();
$response = $this->postJson(route('task.store',['title' => $task->title]))
->assertcreated();
$this->assertDatabaseHas('tasks', ['title' => $task->title]);
}
ტესტი task.store მარშრუტს მოგვთხოვს, ჩავამატოთ იგი routes/api.php ფაილში :
Route::post('task', [TaskController::class,'store'])->name('task.store');
TaskController კონტროლერში კი ჩავამატოთ store მეთოდი :
public function store(Request $request)
{
return Task::create($request->all());
}
public function test_delete_task()
{
$task = Task::factory()->create();
$this->deleteJson(route('task.destroy', $task->id))
->assertNoContent();
$this->assertDatabaseMissing('tasks', ['title' => $task->title]);
}
ტესტი task.destroy მარშრუტს მოგვთხოვს, ჩავამატოთ იგი routes/api.php ფაილში :
Route::delete('task/{task}', [TaskController::class,'destroy'])->name('task.destroy');
TaskController კონტროლერში კი ჩავამატოთ destroy მეთოდი :
public function destroy(Task $task)
{
$task->delete();
return response('', 204);
}
ბარემ აქვე გამოვიყენოთ ჩვენთვის უკვე კარგად ნაცნობი apiResource მეთოდი და routes/api.php ფაილი გადავაკეთოთ შემდეგნაირად :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
use App\Http\Controllers\Api\TaskController;
Route::apiResource('todo-list', TodoListController::class);
Route::apiResource('task', TaskController::class);
სიებსა და დავალებებს შორის განვსაზღვროთ ასეთი ურთიერთკავშირი : ნებისმიერ სიას უნდა გააჩნდეს ერთი ან რამდენიმე დავალება, ნებისმიერ დავალებას კი უნდა ჰყავდეს ერთი მშობელი სია. ამ კავშირიდან გამომდინარე, ცვლილებები მოგვიწევს, როგორც მარშრუტებში, ასევე ტესტებშიც. დავიწყოთ მარშრუტებით. როგორც ვიცით, დავალებებში გვაქვს ხუთი წერტილი :
routes/api.php ფაილში შევიტანოთ ასეთი ცვლილება :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
use App\Http\Controllers\Api\TaskController;
Route::apiResource('todo-list', TodoListController::class);
Route::apiResource('todo-list/{todo_list}/task', TaskController::class);
ეს ცვლილება მოგვცემს ამდაგვარ შედეგს :
GET|HEAD api/todo-list/{todo_list}/task .................................. task.index › Api\TaskController@index
POST api/todo-list/{todo_list}/task .................................. task.store › Api\TaskController@store
GET|HEAD api/todo-list/{todo_list}/task/{task} ............................... task.show › Api\TaskController@show
PUT|PATCH api/todo-list/{todo_list}/task/{task} ........................... task.update › Api\TaskController@update
DELETE api/todo-list/{todo_list}/task/{task} ......................... task.destroy › Api\TaskController@destroy
ასეთ შემთხვევაში დაგვეხმარება ლარაველის ფუნქცია shallow, რომელიც მარშრუტთა ზედაპირულ შერწყმას აკეთებს (ინგ: shallow - არაღრმა, წვრილმანი).
მისი მეშვეობით სისტემა ხვდება თუ სად არის საკმარისი მხოლოდ ერთი იდენტიფიკატორი (ID) და სად არა.
routes/api.php ფაილი :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
use App\Http\Controllers\Api\TaskController;
Route::apiResource('todo-list', TodoListController::class);
Route::apiResource('todo-list.task', TaskController::class)->shallow();
ეს ჩანაწერი მოგვცემს ასეთ შედეგს :
GET|HEAD api/task/{task} ................................................................................... task.show › Api\TaskController@show
PUT|PATCH api/task/{task} ............................................................................... task.update › Api\TaskController@update
DELETE api/task/{task} ............................................................................. task.destroy › Api\TaskController@destroy
GET|HEAD api/todo-list .......................................................................... todo-list.index › Api\TodoListController@index
POST api/todo-list .......................................................................... todo-list.store › Api\TodoListController@store
GET|HEAD api/todo-list/{todo_list} ................................................................ todo-list.show › Api\TodoListController@show
PUT|PATCH api/todo-list/{todo_list} ............................................................ todo-list.update › Api\TodoListController@update
DELETE api/todo-list/{todo_list} .......................................................... todo-list.destroy › Api\TodoListController@destroy
GET|HEAD api/todo-list/{todo_list}/task ........................................................ todo-list.task.index › Api\TaskController@index
POST api/todo-list/{todo_list}/task ........................................................ todo-list.task.store › Api\TaskController@store
ანუ სიებისა და დავალებების თანაკვეთა მოხდება მხოლოდ ამ ორ მარშრუტში :
ტიპი | URI | ქმედება | მარშრუტი |
---|---|---|---|
GET | api/todo-list/{todo_list}/task | index | todo-list.task.index |
POST | api/todo-list/{todo_list}/task | store | todo-list.task.store |
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\Task;
use App\Models\TodoList;
class TaskTest extends TestCase
{
use RefreshDatabase;
private $list;
public function setUp():void
{
parent::setUp();
$this->list = TodoList::factory()->create(['name' => 'my list']);
}
public function test_fetch_tasks_for_todo_list()
{
$task = Task::factory()->create();
$response = $this->getJson(route('todo-list.task.index',$this->list->id))->assertOk()->json();
$this->assertEquals(1, count($response));
$this->assertEquals($task->title, $response[0]['title']);
}
public function test_store_task_for_todo_list()
{
$task = Task::factory()->make();
$response = $this->postJson(route('todo-list.task.store', $this->list->id),['title' => $task->title])
->assertcreated();
$this->assertDatabaseHas('tasks', ['title' => $task->title]);
}
public function test_delete_task()
{
$task = Task::factory()->create();
$this->deleteJson(route('task.destroy', $task->id))
->assertNoContent();
$this->assertDatabaseMissing('tasks', ['title' => $task->title]);
}
}
public function test_update_task()
{
$task = Task::factory()->create();
$response = $this->putJson(route('task.update',$task->id),['title' => 'new title'])
->assertOk();
$this->assertDatabaseHas('tasks', ['title' => 'new title']);
}
TaskController კონტროლერში კი ჩავამატოთ update მეთოდი :
public function update(Request $request, Task $task)
{
$task->update($request->all());
return $task;
}
public function test_store_task_for_todo_list()
{
$task = Task::factory()->make();
$response = $this->postJson(route('todo-list.task.store', $this->list->id),['title' => $task->title])
->assertcreated();
$this->assertDatabaseHas('tasks', [
'title' => $task->title,
'todo_list_id' => $this->list->id
]);
}
TaskController კონტროლერში კი მოვიქცეთ ასე :
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Task;
use App\Models\TodoList;
class TaskController extends Controller
{
public function index(TodoList $todo_list)
{
$tasks = Task::where('todo_list_id', $todo_list->id)->get();
return response($tasks);
}
public function store(Request $request, TodoList $todo_list)
{
$request['todo_list_id'] = $todo_list->id;
return Task::create($request->all());
}
public function destroy(Task $task)
{
$task->delete();
return response('', 204);
}
public function update(Request $request, Task $task)
{
$task->update($request->all());
return $task;
}
}
ამის შემდეგ ტესტი გვეტყვის, რომ tasks ცხრილში არ გვაქვს todo_list_id ველი. database/migrations/{datetime}_create_tasks_table.php :
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('todo_list_id');
$table->string('title');
$table->timestamps();
});
}
ცვლილება მოგვიწევს TaskFactory მოდელმწარმოებელშიც :
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\TodoList;
class TaskFactory extends Factory
{
public function definition()
{
return [
'todo_list_id' => function(){
return TodoList::factory()->create()->id;
},
'title' => $this->faker->sentence()
];
}
}
php artisan make:test TodoListTest --unit
როგორც ვიცით, ეს ბრძანება tests/Unit საქაღალდეში შეგვიქმნის TodoListTest.php ფაილს, შევიტანოთ მასში შემდეგი კოდი :
namespace Tests\Unit;
use Tests\TestCase;
use App\Models\Task;
use App\Models\TodoList;
use Illuminate\Foundation\Testing\RefreshDatabase;
class TodoListTest extends TestCase
{
use RefreshDatabase;
public function test_todo_list_has_many_tasks()
{
$list = TodoList::factory()->create(['name' => 'my list']);
$task = Task::factory()->create(['todo_list_id' => $list->id]);
$this->assertInstanceOf(Task::class, $list->tasks->first());
}
}
შევქმენით სია, ამ სიას დავუკავშირეთ ახალი დავალება, შემდეგ კი შევეცადეთ
დაგვემტკიცებინა, რომ სიის tasks მეთოდით დაბრუნებული
კოლექციის პირველი ელემენტი არის Task კლასის ეგზემპლიარი.
ტესტის შედეგად მივიღებთ ასეთ შეტყობინებას :
Call to a member function first() on null
ანუ სიის tasks მეთოდით არაფერი ბრუნდება, რადგან ეს მეთოდი არც გვაქვს :)) შევქმნათ იგი, TodoList მოდელი :
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TodoList extends Model
{
use HasFactory;
protected $guarded = [];
public function tasks()
{
return $this->hasMany(Task::class);
}
}
ახლა ეს კავშირი აღვწეროთ უკუმუმართულებითაც, ანუ საქმეში ჩავრთოთ belongsTo ფუნქცია. შევქმნათ ახალი ფრაგმენტული ტესტი :
php artisan make:test TaskTest --unit
tests/Unit/TaskTest.php ფაილში შევიტანოთ შემდეგი კოდი :
use Tests\TestCase;
use App\Models\Task;
use App\Models\TodoList;
use Illuminate\Foundation\Testing\RefreshDatabase;
class TaskTest extends TestCase
{
use RefreshDatabase;
public function test_task_belongs_todo_list()
{
$list = TodoList::factory()->create(['name' => 'my list']);
$task = Task::factory()->create(['todo_list_id' => $list->id]);
$this->assertInstanceOf(TodoList::class, $task->todo_list);
}
}
Task მოდელში კი ჩავამატოთ todo_list მეთოდი :
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
use HasFactory;
protected $guarded = [];
public function todo_list()
{
return $this->belongsTo(TodoList::class);
}
}
ახლა უკვე წარმატებით დასრულდება ტესტი.
ზემოთ აღწერილი კავშირები გამოვიყენოთ კონტროლერის კოდის დასახვეწად, TaskController :
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Task;
use App\Models\TodoList;
class TaskController extends Controller
{
public function index(TodoList $todo_list)
{
return response($todo_list->tasks);
}
public function store(Request $request, TodoList $todo_list)
{
return $todo_list->tasks()->create($request->all());
}
public function destroy(Task $task)
{
$task->delete();
return response('', 204);
}
public function update(Request $request, Task $task)
{
$task->update($request->all());
return $task;
}
}
APP_DEBUG=true
ამის შემდეგ გავუშვათ მიგრაციების შესრულების ბრძანება :
php artisan migrate
ამის შემდეგ, იგივე წერტილზე მიკითხვისას, დაგვიბრუნდება ცარიელი კოლექცია. შევქმნათ რაიმე დავალება :
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('todo_list_id');
$table->string('title');
$table->string('status')->default('not_started');
$table->timestamps();
});
}
გავიხსენოთ ჩვენი დევიზი : ჯერ ტესტი, შემდეგ კოდი :))) შევქნათ ტესტი :
php artisan make:test TaskCompletedTest
TaskCompletedTest :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\Task;
class TaskCompletedTest extends TestCase
{
use RefreshDatabase;
public function test_task_status_can_be_changed()
{
$task = Task::factory()->create();
$this->patchJson(route('task.update', $task->id),['status' => 'started']);
$this->assertDatabaseHas('tasks', ['status' => 'started']);
}
}
public function test_if_todo_list_is_deleted_its_tasks_will_be_deleted_()
{
$list = TodoList::factory()->create(['name' => 'my list']);
$task = Task::factory()->create(['todo_list_id' => $list->id]);
$list->delete();
$this->assertDatabaseMissing('todo_lists', ['id' => $list->id]);
$this->assertDatabaseMissing('tasks', ['id' => $task->id]);
}
ბუნებრივია ტესტი წარუმატებელი იქნება. 2022_11_24_222616_create_tasks_table მიგრაციის ფაილში შევიტანოთ შემდეგი ცვლილება :
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('todo_list_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->string('status')->default('not_started');
$table->timestamps();
});
}
php artisan make:test Auth/RegistrationTest
RegistrationTest ფაილში შევიტანოთ შემდეგი კოდი :
namespace Tests\Feature\Auth;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class RegistrationTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_register()
{
$this->postJson(route('user.register'),[
'name' => 'vaso',
'email' => 'vaso@gmail.com',
'password' => '12345',
'password_confirmation' => '12345'
])->assertcreated();
$this->assertDatabaseHas('users', ['name' => 'vaso']);
}
}
ტესტი მოგვთხოვს მარშრუტს, routes/api.php :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
use App\Http\Controllers\Api\TaskController;
use App\Http\Controllers\Api\Auth\RegisterController;
Route::apiResource('todo-list', TodoListController::class);
Route::apiResource('todo-list.task', TaskController::class)->shallow();
Route::post('/register', [RegisterController::class,'register'])->name('user.register');
ამის შემდეგ ტესტი მოგვთხოვს კონტროლერს, შევქმნათ იგი :
php artisan make:controller Api\Auth\RegisterController
ახლადშექმნილ კონტროლერში შევიტანოთ შემდეგი კოდი :
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Http\Requests\RegisterRequest;
class RegisterController extends Controller
{
public function register(RegisterRequest $request)
{
$data = $request->validated();
$data['password'] = bcrypt($request->password);
$user = User::create($data);
return $user;
}
}
რეგისტრაციის ვალიდაციისათვის შევქმნათ შესაბამისი ფაილი :
php artisan make:request RegisterRequest
ფაილში შევიტანოთ შემდეგი კოდი :
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => ['required'],
'email' => ['required','unique:users'],
'password' => ['required','confirmed'],
];
}
}
ამჯერად ტესტი წარმატებით დასრულდება.
php artisan make:test Auth/LoginTest
ახლადშექმნილ ტესტში დროებით შევიტანოთ შემდეგი კოდი :
namespace Tests\Feature\Auth;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\User;
class LoginTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_login()
{
$user = User::factory()->create();
$response = $this->postJson(route('user.login'),[
'email' => $user->email,
'password' => 'password' // User::factory ადებს ამ პაროლს
])->assertOk();
$this->assertArrayHasKey('token', $response->json());
}
public function test_incorrect_email()
{
$this->postJson(route('user.login'),[
'email' => 'incorrect@mail.com',
'password' => '12345'
])->assertUnauthorized();
}
public function test_incorrect_password()
{
$user = User::factory()->create();
$this->postJson(route('user.login'),[
'email' => $user->email,
'password' => 'arasworiparoli'
])->assertUnauthorized();
}
}
routes/api.php :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
use App\Http\Controllers\Api\TaskController;
use App\Http\Controllers\Api\Auth\RegisterController;
use App\Http\Controllers\Api\Auth\LoginController;
Route::apiResource('todo-list', TodoListController::class);
Route::apiResource('todo-list.task', TaskController::class)->shallow();
Route::post('/register', [RegisterController::class,'register'])->name('user.register');
Route::post('/login', [LoginController::class,'login'])->name('user.login');
შევქმნათ კონტროლერი :
php artisan make:controller Api\Auth\LoginController
ახლადშექმნილ კონტროლერში დროებით შევიტანოთ შემდეგი კოდი :
namespace App\Http\Controllers\Api\Auth;
use Hash;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
class LoginController extends Controller
{
public function login(LoginRequest $request)
{
$user = User::where('email', $request->email)->first();
if(!$user || !Hash::check($request->password, $user->password))
{
return response('Incorrect data', Response::HTTP_UNAUTHORIZED);
}
return response([
'token' => 'hello'
]);
}
}
ავტორიზასიის ვალიდაციისათვის შევქმნათ შესაბამისი ფაილი :
php artisan make:request LoginRequest
ფაილში შევიტანოთ შემდეგი კოდი :
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'email' => ['required'],
'password' => ['required'],
];
}
}
ამ ეტაპზე ტესტი წარმატებით გაეშვება.
LoginController-ის login მეთოდში შევიტანოთ შემდეგი ცვლილებები :
public function login(LoginRequest $request)
{
$user = User::where('email', $request->email)->first();
if(!$user || !Hash::check($request->password, $user->password))
{
return response('Incorrect data', Response::HTTP_UNAUTHORIZED);
}
$token = $user->createToken('api');
return response([
'token' => $token->plainTextToken
]);
}
თუ ავტორიზაციის ტესტში დავბეჭდავთ მიღებულ პასუხს :
echo '<pre>';
print_r($response->json());
echo '</pre>';
die;
ვიხილავთ ამდაგვარ სურათს :
[token] => 1|iGoU6BibIECScDUAOZ56SiJ9RrcFXomaF2aPrm1J
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoListController;
use App\Http\Controllers\Api\TaskController;
use App\Http\Controllers\Api\Auth\RegisterController;
use App\Http\Controllers\Api\Auth\LoginController;
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('todo-list', TodoListController::class);
Route::apiResource('todo-list.task', TaskController::class)->shallow();
});
Route::post('/register', [RegisterController::class,'register'])->name('user.register');
Route::post('/login', [LoginController::class,'login'])->name('user.login');
გავუშვათ ტესტები.
namespace Tests;
use App\Models\User;
use Laravel\Sanctum\Sanctum;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
public function setUp(): void
{
parent::setUp();
$this->withoutExceptionHandling();
}
public function createUser($args = [])
{
return User::factory()->create();
}
public function authUser()
{
$user = $this->createUser();
Sanctum::actingAs($user);
return $user;
}
}
tests/Feature/TodoListTest და tests/Feature/TaskTest ტესტების setUp() მეთოდში ჩავამატოთ შემდეგი ჩანაწერი :
public function setUp():void
{
parent::setUp();
$this->authUser();
$this->list = TodoList::factory()->create(['name' => 'my list']);
}
tests/Feature/TaskCompletedTest კლასის test_task_status_can_be_changed მეთოდში კი ჩავამატოთ ეს ჩანაწერი :
public function test_task_status_can_be_changed()
{
$this->authUser();
$task = Task::factory()->create();
$this->patchJson(route('task.update', $task->id),['status' => 'started']);
$this->assertDatabaseHas('tasks', ['status' => 'started']);
}
tests/Feature/Auth/LoginTest ფაილში კი მოვახდინოთ კოდის მცირედი დახვეწა :
namespace Tests\Feature\Auth;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\User;
class LoginTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_login()
{
$user = $this->createUser();
$response = $this->postJson(route('user.login'),[
'email' => $user->email,
'password' => 'password' // User::factory ადებს ამ პაროლს
])->assertOk();
$this->assertArrayHasKey('token', $response->json());
}
public function test_incorrect_email()
{
$this->postJson(route('user.login'),[
'email' => 'incorrect@mail.com',
'password' => '12345'
])->assertUnauthorized();
}
public function test_incorrect_password()
{
$user = $this->createUser();
$this->postJson(route('user.login'),[
'email' => $user->email,
'password' => 'arasworiparoli'
])->assertUnauthorized();
}
}
public function up()
{
Schema::create('todo_lists', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->timestamps();
});
}
შევიტანოთ ცვლილება TodoListFactory მოდელმწარმოებელშიც :
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class TodoListFactory extends Factory
{
public function definition()
{
return [
'name' => $this->faker->sentence,
'user_id' => function(){
return User::factory()->create()->id;
}
];
}
}
ცვლილებების შეტანა მოგვიწევს tests/Feature/TodoListTest ფაილშიც :
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\TodoList;
class TodoListTest extends TestCase
{
use RefreshDatabase;
private $list;
public function setUp():void
{
parent::setUp();
$user = $this->authUser();
$this->list = TodoList::factory()->create([
'name' => 'my list',
'user_id' => $user->id
]);
}
public function test_todo_list_index()
{
// ქმედება
$response = $this->getJson(route('todo-list.index'));
// მტკიცება
$this->assertEquals(1, count($response->json()));
$this->assertEquals('my list', $response->json()[0]['name']);
}
public function test_fetch_single_todo_list()
{
$response = $this->getJson(route('todo-list.show', $this->list->id))
->assertOk()
->json();
$this->assertEquals($response['name'], $this->list->name);
}
public function test_store_new_todo_list()
{
$list = TodoList::factory()->make();
$response = $this->postJson(route('todo-list.store'),[
'name' => $list->name,
'user_id' => $this->authUser()->id
])
->assertCreated()
->json();
$this->assertEquals($list->name, $response['name']);
$this->assertDatabaseHas('todo_lists',['name' => $list->name]);
}
public function test_name_field_validation()
{
/*
* ამ ჩანაწერის გარეშე ლარაველი დაგვიგენერირებს ვალიდაციასთან
* დაკავშირებულ გამონაკლისს და ტესტი არ შესრულდება
*/
$this->withExceptionHandling();
$response = $this->postJson(route('todo-list.store'))
->assertUnprocessable(); // იგივე assertStatus(422)
$response->assertJsonValidationErrors(['name']);
}
public function test_delete_todo_list()
{
// შეხსენება : $this->list აღწერილი გვაქვს TodoListTest კლასის setUp() მეთოდში
$this->deleteJson(route('todo-list.destroy', $this->list))
->assertNoContent(); // იგივე assertStatus(204)
$this->assertDatabaseMissing('todo_lists',['name' => $this->list->name]);
}
public function test_update_todo_list()
{
$this->putJson(route('todo-list.update', $this->list),['name' => 'updated name'])
->assertOk();
$this->assertDatabaseHas('todo_lists',[
'id' => $this->list->id,
'name' => 'updated name'
]);
}
}
ახლა კი დროა სიების ჩამონათვალის წერტილშიც აღვწეროთ ლოგიკა, რომელიც მხოლოდ ავტორიზირებული მომხმარებლის სიებს გამოიტანს, TodoListController
კონტროლერის index მეთოდი :
public function index()
{
$lists = TodoList::where('user_id', auth()->user()->id)->get();
return response($lists);
}
როგორც ადრე აღვნიშნეთ, ერთ მომხმარებელს შეიძლება გააჩნდეს დავალებათა, რამოდენიმე სია, აღვწეროთ ეს კავშირი User მოდელში :
public function todo_lists()
{
return $this->hasMany(TodoList::class);
}
ამ კავშირის გასატესტად შევქმნათ ფრაგმენტული ტესტი :
php artisan make:test UserTest --unit
ახლადშექმნილ ფაილ tests/Unit/UserTest.php ფაილში შევიტანოთ შემდეგი კოდი :
namespace Tests\Unit;
use App\Models\TodoList;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_user_has_many_lists()
{
$user = $this->createUser();
$list = TodoList::factory()->create(['user_id' => $user->id]);
$this->assertInstanceOf(TodoList::class, $user->todo_lists->first());
}
}
კვლავ შევიტანოთ ცვლილებები TodoListController კონტროლერის index და store მეთოდებში :
public function index()
{
return auth()->user()->todo_lists;
}
public function store(TodoListRequest $request)
{
return auth()->user()
->todo_lists()
->create($request->validated());
}
git clone https://github.com/VasilNadiradze/todo-list-rest-api-laravel-tdd.git
cd todo-list-rest-api-laravel-tdd
შემდეგ დააინსტალირეთ კომპოზერის დამოკიდებულებები :
composer install
დააკოპირეთ .env.example ფაილი, ასლს დაარქვით .env და მასში აღწერეთ მონაცემთა ბაზის, თქვენთვის სასურველი პარამეტრები.
დააგენერირეთ აპლიკაციის გასაღები :
php artisan key:generate
გაუშვით პროექტი :
php artisan serve
ასევე განვიხილავთ ისეთ სისტემებსა და ინსტრუმენტებს, რომლებთანაც აქამდე არ გვქონია შეხება. მაგალითად მუშაობა მოგვიწევს ფაილებთან, ქეშირებასთან, ჰეშირებასთან, ასევე ვისწავლით Ajax-თან მუშაობას, დავაინსტალირებთ სხვადასხვა პაკეტებს და ა.შ ...
მომავალი პროექტის ადმინისტრატორის მხარისთვის გამოვიყენოთ Bootstrap ფრეიმვორკის მარტივი შაბლონი, რომლის გადმოწერაც შეგიძლიათ ამ ბმულიდან.
მომხმარებლის მხარისათვის კი გამოვიყენოთ ეს შაბლონი.
მზა პროექტის გადმოსაწერად მიჰყევით ამ ბმულს.
composer create-project laravel/laravel blog
ახლა ავამუშავოთ პროექტი :
cd path/to/project
php artisan serve
ასევე დავაინსტალიროთ საკმაოდ მოსახერხებელი
პაკეტი, რომლის მეშვეობითაც შესაძლებელია თვალი ვადევნოთ გაშვებულ SQL ბრძანებებს, მიმდინარე მარშრუტებს,
კონფიგურაციულ პარამეტრებს და ა.შ.
composer require barryvdh/laravel-debugbar --dev
აპლიკაციის კეთების პროცესში სასურველია, რომ .env ფაილში ჩართული გვქონდეს შეცდომების ხილვის რეჟიმი (debugger) :
APP_DEBUG=true
ასეთ შემთხვევაში, ზემოთხსენებული პაკეტის ინსტალაციის შემდეგ, ბრაუზერში ვიხილავთ ამდაგვარ პანელს :
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
ასე რომ, მომხმარებლების ცხრილის მიგრაციის ფაილი უკვე გვაქვს.
php artisan make:migration create_admins_table
public function up()
{
Schema::create('admins', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->timestamps();
});
}
php artisan make:migration create_articles_table
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('image', 255);
$table->timestamps();
});
}
php artisan make:migration create_articles_translates_table
სათარგმნი ცხრილის თითოეული ჩანაწერი დაკავშირებული უნდა იყოს ძირითადი ცხრილის
კონკრეტულ ჩანაწერთან :
public function up()
{
Schema::create('articles_translates', function (Blueprint $table) {
$table->id();
$table->foreignId('article_id')->constrained()->onDelete('cascade');
$table->string('title', 100);
$table->string('description', 255);
$table->text('text');
$table->string('lang', 2); // ენის ინდექსი : ka, en ...
$table->timestamps();
});
}
php artisan make:migration create_comments_table
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('article_id')->constrained()->onDelete('cascade');
$table->string('comment', 255);
$table->timestamps();
});
}
php artisan make:migration create_contacts_table
public function up()
{
Schema::create('contacts', function (Blueprint $table) {
$table->id();
$table->string('phone', 100);
$table->string('email', 100);
$table->timestamps();
});
}
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=user
DB_PASSWORD=password
php artisan migrate
ჩვენი პროექტის ბაზას dbdiagram.io-ზე ექნება შემდეგი სახე :
Table "admins" {
"id" bigint(20) [not null]
"name" varchar(255) [not null]
"email" varchar(255) [not null]
"password" varchar(255) [not null]
"created_at" timestamp [default: NULL]
"updated_at" timestamp [default: NULL]
}
Table "articles" {
"id" bigint(20) [not null]
"image" varchar(255) [not null]
"created_at" timestamp [default: NULL]
"updated_at" timestamp [default: NULL]
}
Table "articles_translates" {
"id" bigint(20) [not null]
"article_id" bigint(20) [not null, ref: > articles.id]
"title" varchar(100) [not null]
"description" varchar(255) [not null]
"text" text [not null]
"lang" varchar(2) [not null]
"created_at" timestamp [default: NULL]
"updated_at" timestamp [default: NULL]
}
Table "comments" {
"id" bigint(20) [not null]
"user_id" bigint(20) [not null, ref: > users.id]
"article_id" bigint(20) [not null, ref: > articles.id]
"comment" varchar(255) [not null]
"confirmed" int(1) [not null, default: 0]
"created_at" timestamp [default: NULL]
"updated_at" timestamp [default: NULL]
}
Table "contacts" {
"id" bigint(20) [not null]
"phone" varchar(100) [not null]
"email" varchar(100) [not null]
"created_at" timestamp [default: NULL]
"updated_at" timestamp [default: NULL]
}
Table "failed_jobs" {
"id" bigint(20) [not null]
"uuid" varchar(255) [not null]
"connection" text [not null]
"queue" text [not null]
"payload" longtext [not null]
"exception" longtext [not null]
"failed_at" timestamp [not null, default: `current_timestamp()`]
}
Table "migrations" {
"id" int(10) [not null]
"migration" varchar(255) [not null]
"batch" int(11) [not null]
}
Table "password_resets" {
"email" varchar(255) [not null]
"token" varchar(255) [not null]
"created_at" timestamp [default: NULL]
}
Table "personal_access_tokens" {
"id" bigint(20) [not null]
"tokenable_type" varchar(255) [not null]
"tokenable_id" bigint(20) [not null]
"name" varchar(255) [not null]
"token" varchar(64) [not null]
"abilities" text [default: NULL]
"last_used_at" timestamp [default: NULL]
"created_at" timestamp [default: NULL]
"updated_at" timestamp [default: NULL]
}
Table "users" {
"id" bigint(20) [not null]
"name" varchar(255) [not null]
"email" varchar(255) [not null]
"email_verified_at" timestamp [default: NULL]
"password" varchar(255) [not null]
"remember_token" varchar(100) [default: NULL]
"created_at" timestamp [default: NULL]
"updated_at" timestamp [default: NULL]
}
php artisan make:model Admin
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Admin extends Model
{
//
}
php artisan make:seeder AdminSeeder
სიდერში მივმართოთ DB ფასადს და განვსაზღვროთ ცხრილში შესატანი ინფორმაცია :
namespace Database\Seeders;
use DB;
use Hash;
use Illuminate\Database\Seeder;
class AdminSeeder extends Seeder
{
public function run()
{
DB::table('admins')->insert([
[
'name' => 'ვასო',
'email' => 'admin@admin.com',
'password' => Hash::make('admin123')
]
]);
}
}
როგორც ვხედავთ, პაროლის ჰეშირებისათვის გამოვიყენეთ Hash ფასადის make მეთოდი, რომელსაც პარამეტრტად გადავეცით
სასურველი პაროლი - 'admin123'. ჰეშირებული პაროლის გადამოწმებას ვისწავლით ადმინისტრატორის ავტორიზაციის მეთოდის
დაწერისას.
ახლა გავუშვათ ინფორმაციის შეტანის ბრძანება :
php artisan db:seed --class=AdminSeeder
php artisan make:controller Admin/BaseController
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
class BaseController extends Controller
{
//
}
php artisan make:controller Admin/AdminsController --resource
namespace App\Http\Controllers\Admin;
use Illuminate\Http\Request;
class AdminsController extends BaseController
{
// ჩამონათვალის გვერდი
public function index()
{
//
}
// ადმინსტრატორის დამატების ფორმის გვერდი
public function create()
{
//
}
// ადმინსტრატორის შენახვა მბ-ში
public function store(Request $request)
{
//
}
// ადმინსტრატორის რედაქტირების ფორმის გვერდი
public function edit($id)
{
//
}
// ადმინსტრატორის განახლება მბ-ში
public function update(Request $request, $id)
{
//
}
// ადმინსტრატორის წაშლა მბ-ში
public function destroy($id)
{
//
}
}
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\LoginController;
Route::get('/admin/login', [LoginController::class, 'showLogin'])->name('ShowLogin');
ახლა შევქმნათ შესაბამისი კონტროლერი :
php artisan make:controller Admin/LoginController
აღვწეროთ მისი showLogin მეთოდი, რომელიც უზრუნველჰყოფს ავტორიზაციის ფორმის გამოტანას ბრაუზერში :
namespace App\Http\Controllers\Admin;
use Illuminate\Http\Request;
class LoginController extends BaseController
{
public function showLogin(Request $request)
{
/*
თუ სესიაში უკვე შენახულია ავტორიზებული ადმინისტრატორის შესაბამისი
მოდელი გადავიდეთ მთავარ გვერდზე, წინააღმდეგ შემთხვევაში ჩავტვირთოთ
ავტორიზაციის ფორმა (AdminMainPage მარშრუტს შევქმნით ოდნავ ქვემოთ)
*/
return $request->session()->has('admin') ? redirect()->route('AdminMainPage') : view('admin.login');
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>ავტორიზაცია</title>
<link href="{{ asset('assets/admin/css/styles.css') }}" rel="stylesheet" />
</head>
<body class="bg-primary">
<div id="layoutAuthentication">
<div id="layoutAuthentication_content">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-lg border-0 rounded-lg mt-5">
<div class="card-body">
<form>
<div class="form-floating mb-3">
<input class="form-control" type="email"/>
<label>ელ-ფოსტა</label>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="password"/>
<label>Password</label>
</div>
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
<button type="submit" class="btn btn-primary">ავტორიზაცია</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
ყურადღება მივაქციოთ წითლად მონიშნულ ჩანაწერს, ისეთი დამხმარე ფაილებისათვის როგორებიცაა css, js და ა.შ, შევქმნათ
public/assets საქაღალდე და იქ განვათავსოთ ისინი. ადმინისტრატორის მხარის დამხმარე ფაილებისათვის შევქმნათ, კიდევ ერთი - public/assets/admin
საქაღალდე. გადმოწერილი Bootstrap შაბლონიდან დავაკოპიროთ მხოლოდ css საქაღალდე და ჩავაგდოთ ახლადშექმნილ public/assets/admin საქაღალდეში.
ახლა დავუბრუნდეთ asset ფუნქციას, იგი მიმდინარე HTTP/HTTPS მოთხოვნიდან გამომდინარე აგენერირებს ბმულს, public საქაღალდეში არსებულ დამხმარე ფაილებამდე. მაგალითად ამ შემთხვევაში დააგენერირებდა შემდეგ ბმულს :
http://127.0.0.1:8000/assets/admin/css/styles.css
თუ შევალთ http://127.0.0.1:8000/admin/login მისამართზე, ვიხილავთ ავტორიზაციის ფორმას.
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\LoginController;
Route::get('/admin/login', [LoginController::class, 'showLogin'])->name('ShowLogin');
Route::post('/admin/signin', [LoginController::class, 'login'])->name('AdminLogin');
login.blade.php ფაილში არსებულ ფორმაში შევიტანოთ ცვლილებები:
<form method="post" action="{{ route('AdminLogin') }}">
@if($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if(Session::has('login_failed'))
<div class="alert alert-danger">
არასწორი მონაცემები
</div>
@endif
@csrf
<div class="form-floating mb-3">
<input class="form-control" type="email" name="email" value="{{ old('email') }}" />
<label>ელ_ფოსტა</label>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="password" name="password" />
<label>პაროლი</label>
</div>
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
<button type="submit" class="btn btn-primary">ავტორიზაცია</button>
</div>
</form>
ახლა აღვწეროთ LoginController კონტროლერის login მეთოდი :
namespace App\Http\Controllers\Admin;
use Hash;
use App\Models\Admin;
use Illuminate\Http\Request;
class LoginController extends BaseController
{
public function showLogin(Request $request)
{
/*
თუ სესიაში უკვე შენახულია ავტორიზებული ადმინისტრატორის შესაბამისი
მოდელი გადავიდეთ მთავარ გვერდზე, წინააღმდეგ შემთხვევაში ჩავტვირთოთ
ავტორიზაციის ფორმა (AdminMainPage მარშრუტს შევქმნით ოდნავ ქვემოთ)
*/
return $request->session()->has('admin') ? redirect()->route('AdminMainPage') : view('admin.login');
}
public function login(Request $request)
{
// ვალიდაცია
$this->validate( $request , [
'password' => 'required',
'email' => 'required|email'
]);
// ვიპოვოთ ადმინისტრატორი მითითებული ელ-ფოსტით
$admin = Admin::where('email', $request->email)->first();
// თუ ადმინისტრატრორი ვერ მოიძებნა ან მოიძებნა, მაგრამ არ ემთხვევა პაროლი
if(!$admin || ($admin && !Hash::check($request->password, $admin->password)))
{
return redirect()->back()->with('login_failed', true);
}
// შევინახოთ ადმინისტრატორის მოდელი სესიაში
$request->session()->put('admin' , $admin);
return redirect()->route('AdminMainPage');
}
}
როგორც ვხედავთ ჰეშირებული პაროლის გადასამოწმებლად გამოვიყენეთ Hash ფასადის check მეთოდი, რომელსაც
პირველ პარამეტრად გადავეცით მომხმარებლის მიერ აკრეფილი პაროლი, მეორე პარამეტრად კი ელ-ფოსტის მიხედვით ამოღებული
მოდელის password ველის მნიშვნელობა, რომელშიც, თავის დროზე, 'admin123' პაროლი შევინახეთ ჰეშირებული სახით.
თუ გადაცემული პარამეტრები ერთმანეთს დაემთხვევა, check მეთოდი აბრუნებს მნიშვნელობას - true, წინააღმდეგ
შემთხვევაში ბრუნდება მნიშვნელობა false.
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\LoginController;
Route::get('/admin/login', [LoginController::class, 'showLogin'])->name('ShowLogin');
Route::post('/admin/signin', [LoginController::class, 'login'])->name('AdminLogin');
Route::prefix('admin')->group(function () {
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
});
როგორც ვხედავთ ადმინისტრატორის განყოფილების ბმულები გავაერთიანეთ ჯგუფში
პრეფიქსით - 'admin', შესაბამისად, მთავარ გვერდზე მოვხვდებით თუ შევალთ
ამ მისამართზე :
http://127.0.0.1:8000/admin
შევქმნათ resources/views/admin/index.blade.php ფაილი და შევიტანოთ მასში შემდეგი კოდი :
<a href="{{ route('AdminLogout') }}">გასვლა</a>
ასევე შევქმნათ შესაბამისი მარშრუტი სისტემიდან გამოსასვლელად :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\LoginController;
Route::get('/admin/login', [LoginController::class, 'showLogin'])->name('ShowLogin');
Route::post('/admin/signin', [LoginController::class, 'login'])->name('AdminLogin');
Route::get('admin/logout', [LoginController::class, 'logout'])->name('AdminLogout');
Route::prefix('admin')->group(function () {
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
});
public function logout(Request $request)
{
$request->session()->forget('admin');
return redirect()->route('ShowLogin');
}
თუ ახლა ავტორიზაციას გავივლით, ვიხილავთ resources/views/admin/index.blade.php ფაილს, რომელშიც განთავსებულია
სისტემიდან გასვლის ბმული. დავაწვეთ ამ ბმულს, სესიიდან წაიშლება ავტორიზებული ადმინისტრატორის შესაბამისი მოდელი
და გადავმისამართდებით ავტორიზაციის გვერდზე.
http://127.0.0.1:8000/admin
ისევ ვიხილავთ resources/views/admin/index.blade.php ფაილს, ანუ ადმინისტრატორის განყოფილების მთავარ გვერდზე
მოვხვდებით ისე, რომ ავტორიზაცია არ გვექნება გავლილი ...
php artisan make:middleware Admin
შევიტანოთ შესაბამისი ცვლილება app/Http/Kernel.php ფაილში :
...
protected $routeMiddleware = [
...
'admin' => \App\Http\Middleware\Admin::class
];
...
თავად შუამავალში კი გვექნება ამდაგვარი სიტუაცია :
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class Admin
{
public function handle(Request $request, Closure $next)
{
if( !$request->session()->has('admin'))
{
return redirect()->route('ShowLogin');
}
return $next($request);
}
}
ახლადშექმნილი შუამავალი დავაკავშიროთ ადმინისტრატორის განყოფილების იმ მარშრუტებს, რომლებზე მოხვედრაც არ შეიძლება თუ
ადმინსტრატორს ავტორიზაცია არ აქვს გავლილი, routes/web.php ფაილი :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\LoginController;
Route::get('/admin/login', [LoginController::class, 'showLogin'])->name('ShowLogin');
Route::post('/admin/signin', [LoginController::class, 'login'])->name('AdminLogin');
Route::get('admin/logout', [LoginController::class, 'logout'])->name('AdminLogout');
Route::group(['middleware' => ['admin'], 'prefix' => 'admin'], function () {
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
});
ოდნავ დავხვეწოთ ეს კოდი :
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\Admin;
use App\Http\Controllers\Admin\LoginController;
Route::group(['middleware' => ['admin'], 'prefix' => 'admin'], function () {
Route::get('/login', [LoginController::class, 'showLogin'])->withoutMiddleware([Admin::class])->name('ShowLogin');
Route::post('/signin', [LoginController::class, 'login'])->withoutMiddleware([Admin::class])->name('AdminLogin');
Route::get('/logout', [LoginController::class, 'logout'])->name('AdminLogout');
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>@yield('title')</title>
@yield('style')
<link href="{{ asset('assets/admin/css/styles.css') }}" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/js/all.min.js" crossorigin="anonymous"></script>
</head>
<body class="sb-nav-fixed">
<nav class="sb-topnav navbar navbar-expand navbar-dark bg-dark">
<!-- ბრენდის ადგილი -->
<a class="navbar-brand ps-3" href="{{ route('AdminMainPage') }}">Laravel</a>
<!-- მენიუს გადამრთველი -->
<button class="btn btn-link btn-sm order-1 order-lg-0 me-4 me-lg-0" id="sidebarToggle" href="#!">
<i class="fas fa-bars"></i>
</button>
<!-- ზედა მენიუ -->
<ul class="navbar-nav d-md-inline-block ms-auto me-0 me-md-3 my-2 my-md-0">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" id="navbarDropdown" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-user fa-fw"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="{{ route('AdminMainPage') }}">მთავარი</a></li>
<li><hr class="dropdown-divider" /></li>
<li><a class="dropdown-item" href="{{ route('AdminLogout') }}">გასვლა</a></li>
</ul>
</li>
</ul>
</nav>
<div id="layoutSidenav">
<div id="layoutSidenav_nav">
<nav class="sb-sidenav accordion sb-sidenav-dark" id="sidenavAccordion">
<div class="sb-sidenav-menu">
<!-- გვერდითი მენიუ -->
<div class="nav">
<a class="nav-link" href="{{ route('AdminMainPage') }}">
<div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt"></i></div>
მთავარი
</a>
</div>
</div>
<div class="sb-sidenav-footer">
<div class="small">გამარჯობა {{ Session::get('admin')->name }}</div>
</div>
</nav>
</div>
<div id="layoutSidenav_content">
<main>
<!-- ძირითადი შიგთავსი -->
@yield('content')
</main>
<!-- ძირი -->
<footer class="py-4 bg-light mt-auto">
<div class="container-fluid px-4">
<div class="d-flex align-items-center justify-content-between small">
<div class="text-muted">© 2021</div>
<div>
<a href="{{ route('AdminMainPage') }}">ადმინსტრატორის პანელი</a>
</div>
</div>
</div>
</footer>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
@yield('script')
</body>
</html>
ახლა კი resources/views/admin/index.blade.php ფაილში წავშალოთ არსებული კოდი და შევიტანოთ შემდეგი :
@extends('admin.layout')
@section('title','მთავარი')
@section('content')
<div class="container-fluid px-4">
<h1 class="mt-4">მთავარი</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">მთავარი</li>
</ol>
<div class="row">
<!-- აქ განთავსდება მოდულების ბმულები -->
</div>
</div>
@endsection
წითელი ფერით მონიშნულია ის ადგილები სადაც შაბლონიზატორის სინტაქსით დაგვჭირდა ჩარევა, ვფიქრობ ყველაფერი
გასაგებია და ახალი არაფერია ამ სინტაქსში, შესაბამისად დაკონკრეტებაზე აღარ დავკარგოთ დრო.
თუ ახლა შევალთ ადმინისტრატორის პანელის მთავარ გვერდზე, ვიხილავთ ამდაგვარ სურათს :
use App\Http\Controllers\Admin\AdminsController;
Route::resource('admins', AdminsController::class);
როგორც ვიცით, Route ფასადის resource მეთოდი დააგენერირებს მარშრუტებს, შემდეგი მახასიათებლების:
@extends('admin.layout')
@section('title','ადმინსტრატორის დამატება')
@section('content')
<div class="container-fluid px-4">
<h1 class="mt-4">ადმინსტრატორის დამატება</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">ადმინსტრატორის დამატება</li>
</ol>
@if($errors->any())
<div class="row">
<div class="col-md-5 offset-4">
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
@endif
<div class="row">
<div class="col-md-6 offset-3">
<form method="post" action="{{ route('admins.store') }}">
@csrf
<div class="form-group row">
<label class="col-sm-2 col-form-label">სახელი</label>
<div class="col-sm-10">
<input type="text" name="name" value="{{ old('name') }}" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">ელ_ფოსტა</label>
<div class="col-sm-10">
<input type="email" name="email" value="{{ old('email') }}" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">პაროლი</label>
<div class="col-sm-10">
<input type="password" name="password" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<button type="submit" class="btn btn-success">დამატება</button>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
namespace App\Http\Controllers\Admin;
use App\Models\Admin;
use Illuminate\Http\Request;
class AdminsController extends BaseController
{
// ჩამონათვალის გვერდი
public function index()
{
//
}
// ადმინსტრატორის დამატების ფორმის გვერდი
public function create()
{
return view('admin.admins.create');
}
// ადმინსტრატორის შენახვა მბ-ში
public function store(Request $request)
{
// ვალიდაცია
$this->validate($request, [
'name' => 'required|string|max:255',
'password' => 'required|string|min:6|max:255',
'email' => 'required|email|max:255|unique:admins',
]);
$store = Admin::store($request); // true ან false
$request->session()->flash('result', $store);
return redirect()->route('admins.index');
}
// ადმინსტრატორის რედაქტირების ფორმის გვერდი
public function edit($id)
{
//
}
// ადმინსტრატორის განახლება მბ-ში
public function update(Request $request, $id)
{
//
}
// ადმინსტრატორის წაშლა მბ-ში
public function destroy($id)
{
//
}
}
თუ ახლა შევალთ http://127.0.0.1:8000/admin/admins/create მისამართზე, ვიხილავთ ადმინისტრატორის დამატების ფორმას.
'email' => 'required|email|max:255|unique:admins',
unique:admins ჩანაწერი აღნიშნავს, რომ admins ცხრილში email ველის მნიშვნელობა უნდა იყოს უნიკალური თითოეული
ჩანაწერისათვის.
როგორც ვიცით, MVC შაბლონის მიხედვით მონაცემთა ბაზებთან სამუშოდ გამოიყენება მოდელები, ამიტომ ინფორმაციის ბაზაში შენახვის ფუნქციონალი აღვწეროთ მოდელში :
$store = Admin::store($request); // true ან false
Admin მოდელის store მეთოდი კი გამოიყურება ასე :
namespace App\Models;
use Hash;
use Illuminate\Database\Eloquent\Model;
class Admin extends Model
{
protected $fillable = ['name','password'];
public static function store($request)
{
$item = new Admin();
$item->name = $request->name;
$item->email = $request->email;
$item->password = Hash::make($request->password);
return $item->save(); // true/false;
}
}
ამის შემდეგ მოდელის მეთოდის მიერ დაბრუნებულ ლოგიკურ შედეგს ვინახავთ სესიაში და გადავდივართ ადმინისტრატორების
ჩამონათვალის გვერდზე :
$request->session()->flash('result', $store);
return redirect()->route('admins.index');
public function index()
{
$items = Admin::all(); // ყველა ჩანაწერის ამოღება admins ცხრილიდან
return view('admin.admins.index', compact('items')); // მივამაგროთ ინფორმაცია და დავაბრუნოთ წარმოდგენის ფაილი
}
resources/views/admin/admins საქაღალდეში შევქმნათ ფაილი index.blade.php :
@extends('admin.layout')
@section('title','ადმინსტრატორები')
@section('content')
<div class="container-fluid px-4">
<h1 class="mt-4">ადმინსტრატორები</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">
<a href="{{ route('admins.create') }}" class="btn btn-sm btn-success">დამატება</a>
</li>
</ol>
<div class="row">
@if(Session::has('result'))
<div class="col-md-12">
<div class="alert alert-{{ Session::get('result') ? 'success' : 'danger'}}">
ოპერაცია {{ Session::get('result') ? 'წარმატებით' : 'წარუმატებლად'}} დასრულდა
</div>
</div>
@endif
<div class="col-md-12">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">სახელი</th>
<th scope="col">ელ_ფოსტა</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
@foreach($items as $key => $item)
<tr>
<th scope="row">{{ ++$key }}</th>
<td>{{ $item->name }}</td>
<td>{{ $item->email }}</td>
<td>
<a href="{{ route('admins.edit', $item->id) }}" class="btn btn-sm btn-primary" style="float: left; margin-right: 5px">
<i class="fa fa-edit"></i>
</a>
@if($item->id != 1)
<form action="{{ route('admins.destroy', $item->id) }}" method="post">
@csrf
<input type="hidden" name="_method" value="delete">
<a href="#!" class="btn btn-sm btn-danger btn-destroy">
<i class="fa fa-trash"></i>
</a>
</form>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection
@section('script')
<script>
$('.btn-destroy').on('click', function(){
if(confirm('დარწმუნებული ხართ ?'))
{
$(this).parent('form').submit();
}
});
</script>
@endsection
<form action="/example" method="POST">
<input type="hidden" name="_method" value="delete">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
</form>
იგივეს ჩაწერა შეიძლება დირექტივების მეშვეობითაც :
<form action="/example" method="POST">
@method('delete')
@csrf
</form>
სწორედ ეს გავაკეთეთ index.blade.php ფაილის შემდეგ ფრაგმენტში :
@if($item->id != 1)
<form action="{{ route('admins.destroy', $item->id) }}" method="post">
@csrf
<input type="hidden" name="_method" value="delete">
<a href="#!" class="btn btn-sm btn-danger btn-destroy">
<i class="fa fa-trash"></i>
</a>
</form>
@endif
რაც შეეხება პირობით ოპერატორს :
@if($item->id != 1)
...
@endif
ბუნებრივია მომხმარებელს არ უნდა მივცეთ საშუალება, რომ წაშალოს admins ცხრილის ყველა ჩანაწერი, რადგან ამ შემთხვევაში
სისტემიდან გამოსვლის შემდეგ იგი ხელახლა ვეღარ გაივლის ავტორიზაციას ვინაიდან ბაზაში აღარ მოიძებნება აღარცერთი
ადმინსტრატორი :)))
წაშლის სისტემას ასევე მიმაგრებული აქვს მცირედი javascript-იც :
@section('script')
<script>
$('.btn-destroy').on('click', function(){
if(confirm('დარწმუნებული ხართ ?'))
{
$(this).parent('form').submit();
}
});
</script>
@endsection
ანუ წაშლის ღილაკზე დაჭერისას მომხმარებელს ვეკითხებით ნამდვილად სურს თუ არა ჩანაწერის წაშლა და დასტურის შემთხვევაში
ვახდენთ იმ ფორმის გაგზავნას, რომელიც წაშლის მარშრუტთან არის დაკავშირებული.
ჩანაწერის წაშლის მეთოდი AdminsController კონტროლერში გამოიყურება ასე :
public function destroy(Request $request, $id)
{
if($id == 1)
{
return redirect()->back();
}
$delete = Admin::find($id)->delete();
$request->session()->flash('result', $delete);
return redirect()->back();
}
...
<a href="{{ route('admins.edit', $item->id) }}" class="btn btn-sm btn-primary" style="float: left; margin-right: 5px">
<i class="fa fa-edit"></i>
</a>
...
კონტროლერის მეთოდი კი, რომელიც რედაქტირების ფორმის ჩატვირთვას უზრუნველჰყოფს, ასეთია :
public function edit($id)
{
$item = Admin::findOrFail($id);
return view('admin.admins.edit', compact('item'));
}
resources/views/admin/admins საქაღალდეში შევქმნათ edit.blade.php ფაილი შემდეგი კოდით :
@extends('admin.layout')
@section('title','ადმინსტრატორის რედაქტირება')
@section('content')
<div class="container-fluid px-4">
<h1 class="mt-4">მთავარი</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">ადმინსტრატორის რედაქტირება</li>
</ol>
@if($errors->any())
<div class="row">
<div class="col-md-5 offset-4">
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
@endif
@if(Session::has('result'))
<div class="col-md-12">
<div class="alert alert-{{ Session::get('result') ? 'success' : 'danger'}}">
ოპერაცია {{ Session::get('result') ? 'წარმატებით' : 'წარუმატებლად'}} დასრულდა
</div>
</div>
@endif
<div class="row">
<div class="col-md-6 offset-3">
<form method="post" action="{{ route('admins.update', $item->id) }}">
@csrf
@method('put')
<div class="form-group row">
<label class="col-sm-2 col-form-label">სახელი</label>
<div class="col-sm-10">
<input type="text" name="name" value="{{ $item->name }}" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">ელ_ფოსტა</label>
<div class="col-sm-10">
<input type="email" name="email" value="{{ $item->email }}" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">პაროლი</label>
<div class="col-sm-10">
<input type="password" name="password" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<button type="submit" class="btn btn-success">განახლება</button>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
namespace App\Http\Controllers\Admin;
use App\Models\Admin;
use Illuminate\Http\Request;
class AdminsController extends BaseController
{
// ჩამონათვალის გვერდი
public function index()
{
$items = Admin::all();
return view('admin.admins.index', compact('items'));
}
// ადმინსტრატორის დამატების ფორმის გვერდი
public function create()
{
return view('admin.admins.create');
}
// ადმინსტრატორის შენახვა მბ-ში
public function store(Request $request)
{
// ვალიდაცია
$this->validate($request, [
'name' => 'required|string|max:255',
'password' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:admins',
]);
$store = Admin::store($request); // true ან false
$request->session()->flash('result', $store);
return redirect()->route('admins.index');
}
// ადმინსტრატორის რედაქტირების ფორმის გვერდი
public function edit($id)
{
$item = Admin::findOrFail($id);
return view('admin.admins.edit', compact('item'));
}
// ადმინსტრატორის განახლება მბ-ში
public function update(Request $request, $id)
{
// ვალიდაცია
$this->validate($request, [
'name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:admins,email,' . $id,
]);
$item = Admin::findOrFail($id);
$update = Admin::updateItem($request, $item); // true ან false
$request->session()->flash('result', $update);
return redirect()->back();
}
// ადმინსტრატორის წაშლა მბ-ში
public function destroy(Request $request, $id)
{
if($id == 1)
{
return redirect()->back();
}
$delete = Admin::find($id)->delete();
$request->session()->flash('result', $delete);
return redirect()->back();
}
}
განვიხილოთ კონტროლერის update მეთოდი, კერძოდ კი ვალიდაციის ეს ფრაგმენტი :
'email' => 'required|email|max:255|unique:admins,email,' . $id,
როგორც ვიცით, unique:admins ჩანაწერი აღნიშნავს, რომ admins ცხრილში email ველის მნიშვნელობა
უნდა იყოს უნიკალური თითოეული ჩანაწერისათვის. ,' . $id ჩანაწერი კი აღნიშნავს, რომ მომხმარებლის მიერ აკრეფილი
ელ_ფოსტა უნდა გააჩნდეს მხოლოდ ამ id-ის მქონე ჩანაწერს.
შემდეგ ისევ იგივე სქემით მეორდება ყველაფერი რაც ჩანაწერის დამატების მეთოდში გვქონდა, უბრალოდ ამჯერად Admin მოდელის updateItem მეთოდს მივმართავთ, რომელიც გამოიყურება ასე :
namespace App\Models;
use Hash;
use Illuminate\Database\Eloquent\Model;
class Admin extends Model
{
protected $fillable = ['name','password'];
public static function store($request)
{
$item = new Admin();
$item->name = $request->name;
$item->email = $request->email;
$item->password = Hash::make($request->password);
return $item->save();
}
public static function updateItem($request, $item)
{
if ($request->password)
{
$update = $item->update([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password)
]);
}
else
{
$update = $item->update([
'name' => $request->name,
'email' => $request->email
]);
}
return $update;
}
}
ბოლოს ისღა დაგვრჩენია, რომ მოდულის ბმულები ჩავამატოთ resources/views/admin/layout.blade.php და resources/views/admin/index.blade.php ფაილებში, ანუ
გვერდით მენიუში და ადმინისტრატორის განყოფილების მთავარ გვერდზე :
...
<!-- გვერდითი მენიუ -->
<div class="nav">
<a class="nav-link" href="{{ route('AdminMainPage') }}">
<div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt"></i></div>
მთავარი
</a>
<a class="nav-link" href="{{ route('admins.index') }}">
<div class="sb-nav-link-icon"><i class="fas fa-user"></i></div>
ადმინისტრატორები
</a>
</div>
...
...
<div class="container-fluid px-4">
<h1 class="mt-4">მთავარი</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">მთავარი</li>
</ol>
<div class="row">
<div class="col-xl-3 col-md-6">
<div class="card bg-primary text-white mb-4">
<div class="card-body">ადმინსტრატორები</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="{{ route('admins.index') }}">სრულად</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
</div>
</div>
...
php artisan make:seeder ContactSeeder
სიდერში მივმართოთ DB ფასადს და განვსაზღვროთ ცხრილში შესატანი ინფორმაცია :
namespace Database\Seeders;
use DB;
use Illuminate\Database\Seeder;
class ContactSeeder extends Seeder
{
public function run()
{
DB::table('contacts')->insert([
[
'phone' => '557 34 43 05',
'email' => 'vasil.nadiradze@gmail.com'
]
]);
}
}
გავუშვათ ინფორმაციის შეტანის ბრძანება :
php artisan db:seed --class=ContactSeeder
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\Admin;
use App\Http\Controllers\Admin\AdminsController;
use App\Http\Controllers\Admin\ContactsController;
use App\Http\Controllers\Admin\LoginController;
Route::get('/', function () {
return view('welcome');
});
Route::group(['middleware' => ['admin'], 'prefix' => 'admin'], function () {
// ავტორიზაცია და სისტემიდან გასვლა
Route::get('/login', [LoginController::class, 'showLogin'])->withoutMiddleware([Admin::class])->name('ShowLogin');
Route::post('/signin', [LoginController::class, 'login'])->withoutMiddleware([Admin::class])->name('AdminLogin');
Route::get('/logout', [LoginController::class, 'logout'])->name('AdminLogout');
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
// ადმინისტრატორები
Route::resource('admins', AdminsController::class);
// საკონტაქტო ინფორმაციის გვერდი
Route::resource('contacts', ContactsController::class, ['only' => ['edit','update']]);
});
როგორც ვხედავთ მარშრუტები განვსაზღვრეთ, მხოლოდ რედაქტირების გვერდისა და უშუალოდ ინფორმაციის განახლების მეთოდებისათვის.
საკონტაქტო ინფორმაციის რედაქტირებისათვის უნდა მივმართოთ შემდეგ ბმულს:
http://127.0.0.1:8000/admin/contacts/1/edit
contacts ცხრილში ინფორმაცია სიდერის მეშვეობით შევიტანეთ, შესაბამისად პირველი ჩანაწერის id იქნება
1.
php artisan make:controller Admin/ContactsController --resource
namespace App\Http\Controllers\Admin;
use DB;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ContactsController extends Controller
{
public function edit($id)
{
$item = DB::table('contacts')->first();
return view('admin.contact.edit', compact('item'));
}
public function update(Request $request, $id)
{
// ვალიდაცია
$this->validate($request, [
'phone' => 'required|string|max:255',
'email' => 'required|email|max:255'
]);
$update = $update = DB::table('contacts')->where('id',$id)->update([
'phone' => $request->phone,
'email' => $request->email
]);
$request->session()->flash('result', true);
return redirect()->back();
}
}
@extends('admin.layout')
@section('title','საკონტაქტო ინფორმაციის რედაქტირება')
@section('content')
<div class="container-fluid px-4">
<h1 class="mt-4">მთავარი</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">
<a href="" class="btn btn-sm btn-success">
{{ Cache::has('contacts') ? 'ქეშის გასუფთავება' : 'ქეშირება' }}
</a>
</li>
</ol>
@if($errors->any())
<div class="row">
<div class="col-md-5 offset-4">
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
@endif
@if(Session::has('result'))
<div class="col-md-12">
<div class="alert alert-{{ Session::get('result') ? 'success' : 'danger'}}">
ოპერაცია {{ Session::get('result') ? 'წარმატებით' : 'წარუმატებლად'}} დასრულდა
</div>
</div>
@endif
<div class="row">
<div class="col-md-6 offset-3">
<form method="post" action="{{ route('contacts.update', $item->id) }}">
@csrf
@method('put')
<div class="form-group row">
<label class="col-sm-2 col-form-label">ტელ</label>
<div class="col-sm-10">
<input type="text" name="phone" value="{{ $item->phone }}" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">ელ_ფოსტა</label>
<div class="col-sm-10">
<input type="email" name="email" value="{{ $item->email }}" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<button type="submit" class="btn btn-success">განახლება</button>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
ქეშირებების კონფიგურაციული პარამეტრები აღწერილია config/cache.php ფაილში, დავაკვირდეთ მის შემდეგ ფრაგმენტს :
'default' => env('CACHE_DRIVER', 'file'),
ეს ჩანაწერი ნიშნავს, რომ ქეშირებული ინფორმაციები შეინახება ფაილებში.
ქეშირებებთან სამუშაოდ გამოიყენება Cache ფასადი :
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
class SomeController extends Controller
{
public function index()
{
$value = Cache::get('key');
}
}
შეგვიძლია, რომ Cache ფასადის get მეთოდს გადავცეთ დამატებითი პარამეტრიც, რომელსაც სისტემა იმ შემთხვევაში გამოიყენებს თუ ქეშში ვერ მოიძებნება მითითებული
გასაღების შესაბამისი მნიშვნელობა :
$value = Cache::get('key', 'default');
$value = Cache::remember('users', $seconds, function () {
return DB::table('users')->get();
});
ანუ მივმართეთ Cache ფასადის remember მეთოდს, რომელსაც პარამეტრებად გადავეცით: გასაღების დასახელება, რომელსაც მიემაგრება კონკრეტული ქეშირებული ინფორმაცია
('უჯრის დასახერლება', რომელშიც მომხმარებლების შესახებ ამოღებული და ქეშირებული ინფორმაცია შეინახება), ასევე ქეშირების ხანგრძლივობა წამებში და ფუნქცია-დამმუშავებელი,
რომელშიც, უშუალოდ დასაქეში ინფორმაციის დაფიქსირება ხდება.
Cache::forget('key');
Cache::has('key'); // true/false
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\Admin;
use App\Http\Controllers\Admin\AdminsController;
use App\Http\Controllers\Admin\ContactsController;
use App\Http\Controllers\Admin\LoginController;
Route::get('/', function () {
return view('welcome');
});
Route::group(['middleware' => ['admin'], 'prefix' => 'admin'], function () {
// ავტორიზაცია და სისტემიდან გასვლა
Route::get('/login', [LoginController::class, 'showLogin'])->withoutMiddleware([Admin::class])->name('ShowLogin');
Route::post('/signin', [LoginController::class, 'login'])->withoutMiddleware([Admin::class])->name('AdminLogin');
Route::get('/logout', [LoginController::class, 'logout'])->name('AdminLogout');
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
// ადმინისტრატორები
Route::resource('admins', AdminsController::class);
// საკონტაქტო ინფორმაციის გვერდი
Route::resource('contacts', ContactsController::class, ['only' => ['edit','update']]);
Route::get('/contacts/cache', [ContactsController::class, 'cache'])->name('contacts.cache');
});
resources/views/admin/contact/edit.blade.php შაბლონში არსებული ქეშირების ბმული დავაკავშიროთ ახლადშექმნილ მარშრუტთან :
...
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">
<a href="{{ route('contacts.cache') }}" class="btn btn-sm btn-success">
{{ Cache::has('contacts') ? 'ქეშის გასუფთავება' : 'ქეშირება' }}
</a>
</li>
</ol>
...
ახლა შევქმნათ შესაბამისი მეთოდი :
namespace App\Http\Controllers\Admin;
use DB;
use Cache;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ContactsController extends Controller
{
public function edit($id)
{
$item = DB::table('contacts')->first();
return view('admin.contact.edit', compact('item'));
}
public function update(Request $request, $id)
{
// ვალიდაცია
$this->validate($request, [
'phone' => 'required|string|max:255',
'email' => 'required|email|max:255'
]);
$update = $update = DB::table('contacts')->where('id',$id)->update([
'phone' => $request->phone,
'email' => $request->email
]);
$request->session()->flash('result', true);
return redirect()->back();
}
public function cache(Request $request)
{
// თუ საკონტაქტო ინფორმაცია უკვე შენახულია ქეშში
if(Cache::has('contacts'))
{
// წავშალოთ იგი
Cache::forget('contacts');
}
else // თუ არადა
{
// შევინახოთ
Cache::remember('contacts', 3600, function () {
return DB::table('contacts')->first();
});
}
$request->session()->flash('result', true);
return redirect()->back();
}
}
ქეშირებული ინფორმაციები ინახება storage/framework/cache/data საქაღალდეში. თუ საკონტაქტო ინფორმაციის ქეშში შენახვის შემდეგ, კონტროლერის მეთოდში
ამდაგვარ ჩანაწერს შევიტანთ :
public function edit($id)
{
echo '<pre>';
print_r(Cache::get('contacts'));
echo '</pre>';
die;
$item = DB::table('contacts')->first();
return view('admin.contact.edit', compact('item'));
}
ვიხილავთ შემდეგ ინფორმაციას :
stdClass Object
(
[id] => 1
[phone] => 557 34 43 05476
[email] => vasil.nadiradze@gmail.com
[created_at] =>
[updated_at] =>
)
ქეშში შენახულ საკონტაქტო ინფორმაციას გამოვიყენებთ მომხმარებლის მხარის სამუშაოებისას.
ახლა ტრადიციულად, საკონტაქტო ინფორმაციის მოდულის ბმულები ჩავამატოთ resources/views/admin/layout.blade.php და resources/views/admin/index.blade.php ფაილებში, ანუ გვერდით მენიუში და ადმინისტრატორის განყოფილების მთავარ გვერდზე :
...
<!-- გვერდითი მენიუ -->
<div class="nav">
<a class="nav-link" href="{{ route('AdminMainPage') }}">
<div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt"></i></div>
მთავარი
</a>
<a class="nav-link" href="{{ route('admins.index') }}">
<div class="sb-nav-link-icon"><i class="fas fa-user"></i></div>
ადმინისტრატორები
</a>
<a class="nav-link" href="{{ route('contacts.edit', 1) }}">
<div class="sb-nav-link-icon"><i class="fas fa-phone"></i></div>
საკონტაქტო ინფორმაცია
</a>
</div>
...
...
<div class="container-fluid px-4">
<h1 class="mt-4">მთავარი</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">მთავარი</li>
</ol>
<div class="row">
<div class="col-xl-3 col-md-6">
<div class="card bg-primary text-white mb-4">
<div class="card-body">ადმინსტრატორები</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="{{ route('admins.index') }}">სრულად</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-success text-white mb-4">
<div class="card-body">საკონტაქტო ინფორმაცია</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="{{ route('contacts.edit', 1) }}">სრულად</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
</div>
</div>
...
composer require mcamara/laravel-localization
იმისათვის რათა უფრო თვალსაჩინოდ ვნახოთ თუ რა კონფიგურაციულ პარამეტრებს მოიცავს აღნიშნული პაკეტი, გავუშვათ შემდეგი ბრძანება :
php artisan vendor:publish --provider="Mcamara\LaravelLocalization\LaravelLocalizationServiceProvider"
ბრძანების შედეგად შეიქმნება ფაილი config/laravellocalization.php, რომელშიც ბრუნდება ჩვეულებრივი მასივი, რომლის პირველი გასაღებიცაა - supportedLocales,
სადაც შენახულია, პაკეტის ინსტალაციის შემდეგ ხელმისაწვდომი ენების საკმაოდ გრძელი სია. განვაკომენტაროთ ჩვენთვის სასურველი ენების (ქართული, ინგლისური) შესაბამისი ჩანაწერები,
დანარჩენი ყველა კი უნდა იყოს დაკომენტარებული :
...
'supportedLocales' => [
...
'ka' => ['name' => 'Georgian', 'script' => 'Geor', 'native' => 'ქართული', 'regional' => 'ka_GE'],
'en' => ['name' => 'English','script' => 'Latn', 'native' => 'English', 'regional' => 'en_GB'],
...
],
ამავე ფაილში აღწერილი useAcceptLanguageHeader პარამეტრის მნიშვნელობად მივუთითოთ false :
'useAcceptLanguageHeader' => false,
...
'locale' => 'ka',
...
protected $routeMiddleware = [
...
'localize' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class,
'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class,
'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class,
'localeViewPath' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class
];
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\Admin;
use App\Http\Controllers\Admin\AdminsController;
use App\Http\Controllers\Admin\ContactsController;
use App\Http\Controllers\Admin\LoginController;
Route::group(['prefix' => LaravelLocalization::setLocale(),'middleware' => [ 'localeSessionRedirect', 'localizationRedirect', 'localeViewPath' ]], function(){
Route::get('/', function () {
return view('welcome');
});
});
Route::group(['middleware' => ['admin'], 'prefix' => 'admin'], function () {
// ავტორიზაცია და სისტემიდან გასვლა
Route::get('/login', [LoginController::class, 'showLogin'])->withoutMiddleware([Admin::class])->name('ShowLogin');
Route::post('/signin', [LoginController::class, 'login'])->withoutMiddleware([Admin::class])->name('AdminLogin');
Route::get('/logout', [LoginController::class, 'logout'])->name('AdminLogout');
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
// ადმინისტრატორები
Route::resource('admins', AdminsController::class);
// საკონტაქტო ინფორმაციის გვერდი
Route::resource('contacts', ContactsController::class, ['only' => ['edit','update']]);
Route::get('/contacts/cache', [ContactsController::class, 'cache'])->name('contacts.cache');
});
თუ ახლა შევალთ შემდეგ მისამართზე :
http://127.0.0.1:8000
სისტემა ავტომატურად გადაგვამისამართებს, ძირითად ენად მითითებული ენის (ქართულის) შესაბამის ბმულზე :
http://127.0.0.1:8000/ka
php artisan make:model Article
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
//
}
php artisan make:model ArticlesTranslate
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ArticlesTranslate extends Model
{
//
}
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
public function translates()
{
return $this->hasMany(ArticlesTranslate::class);
}
}
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ArticlesTranslate extends Model
{
public function article()
{
return $this->belongsTo(Article::class);
}
}
php artisan make:controller Admin/ArticlesController --resource
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ArticlesController extends BaseController
{
public function index()
{
//
}
public function create()
{
//
}
public function store(Request $request)
{
//
}
public function show($id)
{
//
}
public function edit($id)
{
//
}
public function update(Request $request, $id)
{
//
}
public function destroy($id)
{
//
}
}
namespace App\Http\Controllers\Admin;
use View;
use LaravelLocalization;
use App\Http\Controllers\Controller;
class BaseController extends Controller
{
public function __construct()
{
View::share('locales', LaravelLocalization::getSupportedLocales());
}
}
როგორც ვიცით, LaravelLocalization ფასადი ხელმისაწვდომია მრავალენოვან სისტემასთან სამუშო პაკეტის ინსტალაციის შემდეგ, მისი getSupportedLocales() მეთოდი კი
ხელმისაწვდომ ენებს გვიბრუნებს შემდეგი სახით :
use App\Http\Controllers\Admin\ArticlesController;
Route::resource('articles', ArticlesController::class);
@extends('admin.layout')
@section('title','სიახლის დამატება')
@section('content')
<div class="container-fluid px-4">
<h1 class="mt-4">მთავარი</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">სიახლის დამატება</li>
</ol>
@if($errors->any())
<div class="row">
<div class="col-md-5 offset-4">
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
@endif
<div class="row">
<div class="col-md-6 offset-3">
<form method="post" action="{{ route('articles.store') }}" enctype="multipart/form-data">
@csrf
@foreach($locales as $key => $locale)
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">სათაური ({{ $key }})</label>
<div class="col-sm-10">
<input type="text" name="translates[{{ $key }}][title]" value="{{ old('translates.'.$key.'.title') }}" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">აღწერა ({{ $key }})</label>
<div class="col-sm-10">
<textarea rows="5" name="translates[{{ $key }}][description]" class="form-control">{{ old('translates.'.$key.'.description') }}</textarea>
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">ტექსტი ({{ $key }})</label>
<div class="col-sm-10">
<textarea rows="10" name="translates[{{ $key }}][text]" class="form-control">{{ old('translates.'.$key.'.text') }}</textarea>
</div>
</div>
@endforeach
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">ფოტო</label>
<div class="col-sm-10">
<input type="file" name="image" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<button type="submit" class="btn btn-success">დამატება</button>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
ამ ფორმის მეშვეობით გაიგზავნება შემდეგი შინაარსის მოთხოვნა :
namespace App\Http\Controllers\Admin;
use App\Models\Article;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ArticlesController extends BaseController
{
public function index()
{
//
}
public function create()
{
return view('admin.articles.create');
}
public function store(Request $request)
{
// სათარგმნი ველების ქართულ ენაზე შევსება აუცულებელია
$this->validate($request,[
'translates.ka.title' => 'required|max:100',
'translates.ka.description' => 'required|max:255',
'translates.ka.text' => 'required',
'image' => 'required|mimes:jpeg,jpg,png',
]);
$store = Article::store($request); // true ან false
$request->session()->flash('result', $store);
return redirect()->route('articles.index');
}
public function show($id)
{
//
}
public function edit($id)
{
//
}
public function update(Request $request, $id)
{
//
}
public function destroy($id)
{
//
}
}
თუ ახლა შევალთ http://127.0.0.1:8000/admin/articles/create მისამართზე, ვიხილავთ სიახლის დამატების ფორმას.
როგორც ვიცით, MVC შაბლონის მიხედვით მონაცემთა ბაზებთან სამუშოდ გამოიყენება მოდელები, ამიტომ ინფორმაციის ბაზაში შენახვის ფუნქციონალი აღვწეროთ მოდელში :
$store = Article::store($request); // true ან false
Article მოდელის store მეთოდი კი გამოიყურება ასე :
namespace App\Models;
use App\Models\ArticlesTranslate;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
public function translates()
{
return $this->hasMany(ArticlesTranslate::class);
}
public static function store($request)
{
$item = new Article;
// აქ ისე ვერ მოვხვდებით, რომ ფოტო არჩეული არ იყოს, მაგრამ მაინც გადავამოწმოთ :))
if ($request->hasFile('image'))
{
$destination = 'uploads/articles'; // სად ვტვირთავთ ფოტოს
$extension = $request->file('image')->getClientOriginalExtension(); // ატვირთული ფაილის გაფართოება : jpeg,jpg,png
$file_name = mt_rand(11111, 99999) . time() . '.' . $extension; // მაგ: 564564564564.jpg
$file_src = '/' . $destination . '/'. $file_name; // მაგ: /uploads/articles/564564564564.jpg
// თუ სამიზნე საქაღალდე არ არსებობს
if (!file_exists($destination))
{
mkdir($destination, 0777, true); // შეიქმნება public/uploads/articles საქაღალდე
}
$request->file('image')->move($destination, $file_name); // ფაილის ატვირთვა სამიზნე საქაღალდეში
$item->image = $file_src; // image ველის განსაზღვრა მოდელის ობიექტისათვის
}
/*
სათარგმნი ინფორმაციების დამუშავების სქემა რომ უფრო მარტივი აღსაქმელი იყოს,
აქვე მოვიყვანოთ მაგალითი თუ რა სახით შედის ეს ინფორმაციები მოთხოვნის ტანში :
[translates] => Array
(
[ka] => Array
(
[title] => სატესტო სიახლის სათაური
[description] => სატესტო სიახლის აღწერა
[text] => სატესტო სიახლის სრული ტექსტი
)
[en] => Array
(
[title] => Test article title
[description] => Test article description
[text] => Test article full text
)
)
*/
// თუ ჩანაწრი წარმატებით შეინახება articles ცხრილში
if ($item->save())
{
// თარგმანების შემცველი ასცოციაციური მასივი ინდექსებით ka,en
$translates = $request->translates;
foreach ($translates as $lang => $translation_data)
{
// სათარგმნი მოდელის ეგზემპლიარი თითოეული ენისათვის
$item_translate = new ArticlesTranslate;
/*
* უშუალოდ თარგმანების მასივი [ველის_დასახელება => თარგმანი_შესაბამის_ენაზე]
* $k : ველის დასახელება, მაგ. 'title'
* $v : თარგმანი შესაბამის ენაზე, მაგ. 'სათური'
*/
foreach($translation_data as $k => $v)
{
/*
თუ რომელიმე სათარგმნი ველი არ შეიყვანა ქართული ენის გარდა რომელიმე სხვა ენაზე
არაკრეფილის მნიშვნელობად ჩაჯდეს ქართული ენის შესაბამისი მნიშვნელობა, ქართულად
ყველა შემთხვევაში აკრეფილი იქნება ინფორმაცია, რადგან ეს ვალიდაციაში გვაქვს მოთხოვნილი
*/
if(!$v)
{
$item_translate->$k = $translates['ka'][$k];
}
else
{
$item_translate->$k = $v;
}
}
$item_translate->lang = $lang;
$item_translate->article_id = $item->id;
$item_translate->save(); // ჩანაწრის შენახვა articles_translates ცხრილში
}
return true;
}
return false;
}
}
ამის შემდეგ მოდელის მეთოდის მიერ დაბრუნებულ ლოგიკურ შედეგს ვინახავთ სესიაში და გადავდივართ სიახლეების ჩამონათვალის გვერდზე :
$request->session()->flash('result', $store);
return redirect()->route('articles.index');
public function index()
{
$items = Article::all('ka');
return view('admin.articles.index', compact('items')); // მივამაგროთ ინფორმაცია და დავაბრუნოთ წარმოდგენის ფაილი
}
Article მოდელის all() მეთოდი კი იქნება შემდეგნაირი :
public static function all($local = null)
{
return Article::join('articles_translates', 'articles.id', '=', 'articles_translates.article_id')
->where('articles_translates.lang', $local)
->select('articles.*', 'articles_translates.title')
->orderBy('id', 'desc')
->get();
}
ეს ჩანაწერი დააგენერირებდა შემდეგ ბრძანებას :
SELECT articles.*, articles_translates.title
FROM articles
INNER JOIN articles_translates
ON articles.id = articles_translates.article_id
WHERE articles_translates.lang = 'ka'
ORDER BY id desc
resources/views/admin/articles საქაღალდეში შევქმნათ ფაილი index.blade.php :
@extends('admin.layout')
@section('title','სიახლეები')
@section('content')
<div class="container-fluid px-4">
<h1 class="mt-4">სიახლეები</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">
<a href="{{ route('articles.create') }}" class="btn btn-sm btn-success">დამატება</a>
</li>
</ol>
<div class="row">
@if(Session::has('result'))
<div class="col-md-12">
<div class="alert alert-{{ Session::get('result') ? 'success' : 'danger'}}">
ოპერაცია {{ Session::get('result') ? 'წარმატებით' : 'წარუმატებლად'}} დასრულდა
</div>
</div>
@endif
<div class="col-md-12">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">სათაური</th>
<th scope="col">ფოტო</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
@foreach($items as $key => $item)
<tr>
<th scope="row">{{ ++$key }}</th>
<td>{{ $item->title }}</td>
<td>
<img src="{{ $item->image }}" style="width: 50px; height: 50px;">
</td>
<td>
<a href="{{ route('articles.edit', $item->id) }}" class="btn btn-sm btn-primary" style="float: left; margin-right: 5px">
<i class="fa fa-edit"></i>
</a>
<form action="{{ route('articles.destroy', $item->id) }}" method="post">
@csrf
<input type="hidden" name="_method" value="delete">
<a href="#!" class="btn btn-sm btn-danger btn-destroy">
<i class="fa fa-trash"></i>
</a>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection
@section('script')
<script>
$('.btn-destroy').on('click', function(){
if(confirm('დარწმუნებული ხართ ?'))
{
$(this).parent('form').submit();
}
});
</script>
@endsection
კონტროლერის მეთოდი გვაქვს, მარშრუტიც - თავისთავად, წარმოდგენის ფაილიც შევქმენით. ისღა დაგვრჩენია სიახლეების
ჩამონათვალის გვერდის სანახავად შევიდეთ შემდეგ მისამართზე :
http://127.0.0.1:8000/admin/articles
ჩანაწერის წაშლის მეთოდი კი ArticlesController კონტროლერში გამოიყურება ასე :
public function destroy(Request $request, $id)
{
$delete = Article::find($id)->delete();
$request->session()->flash('result', $delete);
return redirect()->back();
}
...
<a href="{{ route('articles.edit', $item->id) }}" class="btn btn-sm btn-primary" style="float: left; margin-right: 5px">
<i class="fa fa-edit"></i>
</a>
...
კონტროლერის მეთოდი კი, რომელიც რედაქტირების ფორმის ჩატვირთვას უზრუნველჰყოფს, ასეთია :
public function edit($id)
{
$items_with_translates = Article::itemByIdWithTranslates($id);
if($items_with_translates->count() != 2)
{
redirect()->route('articles.index');
}
return view('admin.articles.edit', compact('items_with_translates')) ;
}
Article მოდელის itemByIdWithTranslates() მეთოდი იქნება ასეთი:
public static function itemByIdWithTranslates($id = null)
{
return Article::join('articles_translates', 'articles.id', '=', 'articles_translates.article_id')
->select('articles.*', 'articles_translates.title', 'articles_translates.description', 'articles_translates.text','articles_translates.lang')
->where('articles.id', $id)
->get();
}
მეთოდში არსებული ჩანაწერის შედეგად დაგენერირდება შემდეგი ბრძანება :
SELECT articles.*, articles_translates.title, articles_translates.description, articles_translates.text, articles_translates.lang
FROM articles
INNER JOIN articles_translates
ON articles.id = articles_translates.article_id
WHERE articles.id = '2'
itemByIdWithTranslates() მეთოდი დააბრუნებს ჩანაწერების კოლექციას, რომლის სიგრძეც, წესით უნდა იყოს 2, რადგანაც სისტემაში ორი ხელმისაწვდომი ენა გვაქვს. შესაბამისად -
articles ცხრილის ერთ ჩანაწერსაც უნდა შეესაბამებოდეს articles_translates ცხრილის 2 ჩანაწერი :
public function edit($id)
{
$items_with_translates = Article::itemsWithTranslates($id);
echo '<pre>';
print_r($items_with_translates>toArray());
echo '</pre>';
die;
...
}
შედეგი იქნება ამდაგვარი :
Array
(
[0] => Array
(
[id] => 2
[image] => /uploads/articles/999131625661813.jpg
[created_at] => 2021-07-07T12:43:33.000000Z
[updated_at] => 2021-07-07T12:43:33.000000Z
[title] => მესამე
[description] => xdfgd
[text] => fgdfgdf
[lang] => ka
)
[1] => Array
(
[id] => 2
[image] => /uploads/articles/999131625661813.jpg
[created_at] => 2021-07-07T12:43:33.000000Z
[updated_at] => 2021-07-07T12:43:33.000000Z
[title] => gdfg
[description] => dfgdfg
[text] => fgdfg
[lang] => en
)
)
ბუნებრივია, რომ სათარგმნ ცხრილებთან მუშაობისას შეგვეძლო ცხრილებს შორის ურთიერთკავშირების გამოყენებაც, Article
და ArticlesTranslate მოდელებში უკვე აღწერილი გვაქვს ეს ურთიერთკავშირები :
public function translates()
{
return $this->hasMany(ArticlesTranslate::class);
}
public function article()
{
return $this->belongsTo(Article::class);
}
...
$items_with_translates = ArticlesTranslate::with('article')->get();
...
თუმცა, როგორც ვხედავთ, გამოვიყენეთ Join ფუნქცია. ამას აქვს ორი მიზეზი : პირველი ის, რომ უფრო გავუშინაურდეთ
ამ ფუნქციას და მეორე - ამ შემთხვევაში ერთ SQL ბრძანებაში ჩავეტიეთ თარგმანების მისაღებად. თქვენ შეგიძლიათ მოიქცეთ
თქვენი შეხედულებისამებრ.
resources/views/admin/articles საქაღალდეში შევქმნათ edit.blade.php ფაილი შემდეგი კოდით :
@extends('admin.layout')
@section('title','სიახლის რედაქტირება')
@section('content')
<div class="container-fluid px-4">
<h1 class="mt-4">მთავარი</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">სიახლის დამატება</li>
</ol>
@if($errors->any())
<div class="row">
<div class="col-md-5 offset-4">
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
@endif
<div class="row">
@if(Session::has('result'))
<div class="col-md-12">
<div class="alert alert-{{ Session::get('result') ? 'success' : 'danger'}}">
ოპერაცია {{ Session::get('result') ? 'წარმატებით' : 'წარუმატებლად'}} დასრულდა
</div>
</div>
@endif
<div class="col-md-6 offset-3">
<form method="post" action="{{ route('articles.update',$items_with_translates->first()->id) }}" enctype="multipart/form-data">
@csrf
@method('put')
@foreach($locales as $key => $locale)
@php
$current_locale_item = $items_with_translates->firstWhere('lang',$key);
@endphp
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">სათაური ({{ $key }})</label>
<div class="col-sm-10">
<input type="text" name="translates[{{ $key }}][title]" value="{{ $current_locale_item->title }}" class="form-control">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">აღწერა ({{ $key }})</label>
<div class="col-sm-10">
<textarea rows="5" name="translates[{{ $key }}][description]" class="form-control">{{ $current_locale_item->description }}</textarea>
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">ტექსტი ({{ $key }})</label>
<div class="col-sm-10">
<textarea rows="10" name="translates[{{ $key }}][text]" class="form-control">{{ $current_locale_item->text }}</textarea>
</div>
</div>
@endforeach
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label">ფოტო</label>
<div class="col-sm-10">
<input type="file" name="image" class="form-control">
<img src="{{ $items_with_translates->first()->image }}" width="100%">
</div>
</div>
<div class="form-group row mt-4">
<label class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<button type="submit" class="btn btn-success">განახლება</button>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
განვიხილოთ ამ კოდის ზოგიერთი ფრაგმენტი. დავაკვირდეთ ფორმის action ატრიბუტს :
action="{{ route('articles.update',$items_with_translates->first()->id) }}"
როგორც ვიცით, განახლების მარშრუტს აუცილებელ პარამეტრად უნდა გადაეცეს შესაბამისი ჩანაწერის იდენტიფიკატორი.
$items_with_translates ცვლადში კი შენახული გვაქვს ორელემენტიანი კოლექცია (კოლექცია მასივადაა დაფორმატებული და
ისეა დაბეჭდილი):
Array
(
[0] => Array
(
[id] => 2
[image] => /uploads/articles/999131625661813.jpg
[created_at] => 2021-07-07T12:43:33.000000Z
[updated_at] => 2021-07-07T12:43:33.000000Z
[title] => მესამე
[description] => xdfgd
[text] => fgdfgdf
[lang] => ka
)
[1] => Array
(
[id] => 2
[image] => /uploads/articles/999131625661813.jpg
[created_at] => 2021-07-07T12:43:33.000000Z
[updated_at] => 2021-07-07T12:43:33.000000Z
[title] => gdfg
[description] => dfgdfg
[text] => fgdfg
[lang] => en
)
)
როგორც ადრეც ვთქვით, ამ კოლექციაში შენახულია, ერთად მოქცეული სათარგმნი და არასათარგმნი ინფორმაციები ხელმისაწვდომი
ენების მიხედვით. არასათარგმნი ველებია : id, image, created_at, updated_at და მათი მნიშვნელობები კოლექციის ორივე
ელემენტისათვის, რა თქმა უნდა, ერთნაირია. ამიტომ სულ ერთია რომელი მათგანიდან ავიღებთ ჩვენთვის საჭირო მნიშვნელობებს
(ამ შემთცვევაში ჩვენ გვჭირდება id). ამიტომ $items_with_translates->first() ჩანაწერით მივწვდით
კოლექციის პირველ ელემენტს და იქიდან ავიღეთ საჭირო ინფორმაცია.
ახლა დავაკვირდეთ შემდეგ ფრაგმენტს :
@php
$current_locale_item = $items_with_translates->firstWhere('lang',$key);
@endphp
ამ ჩანაწერით ხდება foreach ციკლში გატარებული ხელმისაწვდომი ენებიდან, ციკლის მიმინარე იტერაციის შესაბამისი
ინფორმაციის ამოღება ზემოთნახსენები ორელემენტიანი კოლექციიდან.
public function update(Request $request, $id)
{
// სათარგმნი ველების ქართულ ენაზე შევსება აუცულებელია
$this->validate($request,[
'translates.ka.title' => 'required|max:100',
'translates.ka.description' => 'required|max:255',
'translates.ka.text' => 'required',
'image' => 'mimes:jpeg,jpg,png', // ფოტოს არჩევა აღარაა აუცილებელი, თუმცა თუ აირჩევს ფორმატი უნდა გადამოწმდეს
]);
$item = Article::findOrFail($id);
$update = Article::updateItem($request, $item); // true ან false
$request->session()->flash('result', $update);
return redirect()->back();
}
შემდეგ ისევ იგივე სქემით მეორდება ყველაფერი რაც ჩანაწერის დამატების მეთოდში გვქონდა, უბრალოდ ამჯერად Article მოდელის updateItem მეთოდს მივმართავთ, რომელიც გამოიყურება ასე :
public static function updateItem($request, $item)
{
if ($request->hasFile('image'))
{
$destination = 'uploads/articles'; // სად ვტვირთავთ ფოტოს
$extension = $request->file('image')->getClientOriginalExtension(); // ატვირთული ფაილის გაფართოება : jpeg,jpg,png
$file_name = mt_rand(11111, 99999) . time() . '.' . $extension; // მაგ: 564564564564.jpg
$file_src = '/' . $destination . '/'. $file_name; // მაგ: /uploads/articles/564564564564.jpg
// თუ სამიზნე საქაღალდე არ არსებობს
if (!file_exists($destination))
{
mkdir($destination, 0777, true); // შეიქმნება public/uploads/articles საქაღალდე
}
$request->file('image')->move($destination, $file_name); // ფაილის ატვირთვა სამიზნე საქაღალდეში
$item->image = $file_src; // image ველის განსაზღვრა მოდელის ობიექტისათვის
}
// თუ ძირითადი ცხრილის ჩანაწერი განახლდა
if ($item->update())
{
$translates = $request->translates;
foreach ($translates as $lang => $translation_data)
{
// სათარგმნი მოდელის, მიმდინარე ენის შესაბამისი ეგზემპლიარი
$item_translate = ArticlesTranslate::where('article_id', $item->id)->where('lang', $lang)->first();
foreach($translation_data as $k => $v)
{
if(!$v)
{
$item_translate->$k = $translates['ka'][$k];
}
else
{
$item_translate->$k = $v;
}
}
$item_translate->update(); // ჩანაწრის განახლება articles_translates ცხრილში
}
return true;
}
return false;
}
საბოლოოდ Article მოდელი მიიღებს შემდეგ სახეს :
namespace App\Models;
use App\Models\ArticlesTranslate;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $guarded = [];
public function translates()
{
return $this->hasMany(ArticlesTranslate::class);
}
public static function store($request)
{
$item = new Article;
// აქ ისე ვერ მოვხვდებით, რომ ფოტო არჩეული არ იყოს, მაგრამ მაინც გადავამოწმოთ :))
if ($request->hasFile('image'))
{
$destination = 'uploads/articles'; // სად ვტვირთავთ ფოტოს
$extension = $request->file('image')->getClientOriginalExtension(); // ატვირთული ფაილის გაფართოება : jpeg,jpg,png
$file_name = mt_rand(11111, 99999) . time() . '.' . $extension; // მაგ: 564564564564.jpg
$file_src = '/' . $destination . '/'. $file_name; // მაგ: /uploads/articles/564564564564.jpg
// თუ სამიზნე საქაღალდე არ არსებობს
if (!file_exists($destination))
{
mkdir($destination, 0777, true); // შეიქმნება public/uploads/articles საქაღალდე
}
$request->file('image')->move($destination, $file_name); // ფაილის ატვირთვა სამიზნე საქაღალდეში
$item->image = $file_src; // image ველის განსაზღვრა მოდელის ობიექტისათვის
}
/*
სათარგმნი ინფორმაციების დამუშავების სქემა რომ უფრო მარტივი აღსაქმელი იყოს,
აქვე მოვიყვანოთ მაგალითი თუ რა სახით შედის ეს ინფორმაციები მოთხოვნის ტანში :
[translates] => Array
(
[ka] => Array
(
[title] => სატესტო სიახლის სათაური
[description] => სატესტო სიახლის აღწერა
[text] => სატესტო სიახლის სრული ტექსტი
)
[en] => Array
(
[title] => Test article title
[description] => Test article description
[text] => Test article full text
)
)
*/
// თუ ჩანაწრი წარმატებით შეინახება articles ცხრილში
if ($item->save())
{
// თარგმანების შემცველი ასცოციაციური მასივი ინდექსებით ka,en
$translates = $request->translates;
foreach ($translates as $lang => $translation_data)
{
// სათარგმნი მოდელის ეგზემპლიარი თითოეული ენისათვის
$item_translate = new ArticlesTranslate;
/*
* უშუალოდ თარგმანების მასივი [ველის_დასახელება => თარგმანი_შესაბამის_ენაზე]
* $k : ველის დასახელება, მაგ. 'title'
* $v : თარგმანი შესაბამის ენაზე, მაგ. 'სათური'
*/
foreach($translation_data as $k => $v)
{
/*
თუ რომელიმე სათარგმნი ველი არ შეიყვანა ქართული ენის გარდა რომელიმე სხვა ენაზე
არაკრეფილის მნიშვნელობად ჩაჯდეს ქართული ენის შესაბამისი მნიშვნელობა, ქართულად
ყველა შემთხვევაში აკრეფილი იქნება ინფორმაცია, რადგან ეს ვალიდაციაში გვაქვს მოთხოვნილი
*/
if(!$v)
{
$item_translate->$k = $translates['ka'][$k];
}
else
{
$item_translate->$k = $v;
}
}
$item_translate->lang = $lang;
$item_translate->article_id = $item->id;
$item_translate->save(); // ჩანაწრის შენახვა articles_translates ცხრილში
}
return true;
}
return false;
}
public static function all($local = null)
{
return Article::join('articles_translates', 'articles.id', '=', 'articles_translates.article_id')
->where('articles_translates.lang', $local)
->select('articles.*', 'articles_translates.title')
->orderBy('id', 'desc')
->get();
}
public static function itemByIdWithTranslates($id = null)
{
return Article::join('articles_translates', 'articles.id', '=', 'articles_translates.article_id')
->select('articles.*', 'articles_translates.title', 'articles_translates.description', 'articles_translates.text','articles_translates.lang')
->where('articles.id', $id)
->get();
}
public static function itemsByIdWithTranslates($local = null)
{
return Article::join('articles_translates', 'articles.id', '=', 'articles_translates.article_id')
->where('articles_translates.lang', $local)
->select('articles.*', 'articles_translates.title', 'articles_translates.description', 'articles_translates.text')
->orderBy('id', 'desc')
->get();
}
public static function updateItem($request, $item)
{
if ($request->hasFile('image'))
{
$destination = 'uploads/articles'; // სად ვტვირთავთ ფოტოს
$extension = $request->file('image')->getClientOriginalExtension(); // ატვირთული ფაილის გაფართოება : jpeg,jpg,png
$file_name = mt_rand(11111, 99999) . time() . '.' . $extension; // მაგ: 564564564564.jpg
$file_src = '/' . $destination . '/'. $file_name; // მაგ: /uploads/articles/564564564564.jpg
// თუ სამიზნე საქაღალდე არ არსებობს
if (!file_exists($destination))
{
mkdir($destination, 0777, true); // შეიქმნება public/uploads/articles საქაღალდე
}
$request->file('image')->move($destination, $file_name); // ფაილის ატვირთვა სამიზნე საქაღალდეში
$item->image = $file_src; // image ველის განსაზღვრა მოდელის ობიექტისათვის
}
if ($item->update())
{
$translates = $request->translates;
foreach ($translates as $lang => $translation_data)
{
$item_translate = ArticlesTranslate::where('article_id', $item->id)->where('lang', $lang)->first();
foreach($translation_data as $k => $v)
{
if(!$v)
{
$item_translate->$k = $translates['ka'][$k];
}
else
{
$item_translate->$k = $v;
}
}
$item_translate->update(); // ჩანაწრის შენახვა articles_translates ცხრილში
}
return true;
}
return false;
}
}
ArticlesController კონტროლერი კი შემდეგს :
namespace App\Http\Controllers\Admin;
use App\Models\Article;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ArticlesController extends BaseController
{
public function index()
{
$items = Article::all('ka');
return view('admin.articles.index', compact('items')); // მივამაგროთ ინფორმაცია და დავაბრუნოთ წარმოდგენის ფაილი
}
public function create()
{
return view('admin.articles.create');
}
public function store(Request $request)
{
// სათარგმნი ველების ქართულ ენაზე შევსება აუცულებელია
$this->validate($request,[
'translates.ka.title' => 'required|max:100',
'translates.ka.description' => 'required|max:255',
'translates.ka.text' => 'required',
'image' => 'required|mimes:jpeg,jpg,png',
]);
$store = Article::store($request); // true ან false
$request->session()->flash('result', $store);
return redirect()->route('articles.index');
}
public function show($id)
{
//
}
public function edit($id)
{
$items_with_translates = Article::itemByIdWithTranslates($id);
if($items_with_translates->count() != 2)
{
redirect()->route('articles.index');
}
return view('admin.articles.edit', compact('items_with_translates')) ;
}
public function update(Request $request, $id)
{
// სათარგმნი ველების ქართულ ენაზე შევსება აუცულებელია
$this->validate($request,[
'translates.ka.title' => 'required|max:100',
'translates.ka.description' => 'required|max:255',
'translates.ka.text' => 'required',
'image' => 'mimes:jpeg,jpg,png', // ფოტოს არჩევა აღარაა აუცილებელი, თუმცა თუ აირჩევს ფორმატი უნდა გადამოწმდეს
]);
$item = Article::findOrFail($id);
$update = Article::updateItem($request, $item); // true ან false
$request->session()->flash('result', $update);
return redirect()->back();
}
public function destroy(Request $request, $id)
{
$delete = Article::find($id)->delete();
$request->session()->flash('result', $delete);
return redirect()->back();
}
}
ბოლოს ისღა დაგვრჩენია, რომ სიახლეების მოდულის ბმულები ჩავამატოთ resources/views/admin/layout.blade.php და resources/views/admin/index.blade.php ფაილებში, ანუ
გვერდით მენიუში და ადმინისტრატორის განყოფილების მთავარ გვერდზე :
...
<!-- გვერდითი მენიუ -->
<div class="nav">
<a class="nav-link" href="{{ route('AdminMainPage') }}">
<div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt"></i></div>
მთავარი
</a>
<a class="nav-link" href="{{ route('admins.index') }}">
<div class="sb-nav-link-icon"><i class="fas fa-user"></i></div>
ადმინისტრატორები
</a>
<a class="nav-link" href="{{ route('contacts.edit', 1) }}">
<div class="sb-nav-link-icon"><i class="fas fa-phone"></i></div>
საკონტაქტო ინფორმაცია
</a>
<a class="nav-link" href="{{ route('articles.index') }}">
<div class="sb-nav-link-icon"><i class="fas fa-newspaper"></i></div>
სიახლეები
</a>
</div>
...
...
<div class="container-fluid px-4">
<h1 class="mt-4">მთავარი</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">მთავარი</li>
</ol>
<div class="row">
<div class="col-xl-3 col-md-6">
<div class="card bg-primary text-white mb-4">
<div class="card-body">ადმინსტრატორები</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="{{ route('admins.index') }}">სრულად</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-success text-white mb-4">
<div class="card-body">საკონტაქტო ინფორმაცია</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="{{ route('contacts.edit', 1) }}">სრულად</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-warning text-white mb-4">
<div class="card-body">სიახლეები</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="{{ route('articles.index') }}">სრულად</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
</div>
</div>
...
php artisan make:controller Front/IndexController
namespace App\Http\Controllers\Front;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class IndexController extends Controller
{
//
}
კონტროლერში შევქმნათ index მეთოდი, რომელიც მომხმარებლის მხარის მთავარი გვერდის
ჩატვირთვას უზრუნველჰყოფს :
namespace App\Http\Controllers\Front;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class IndexController extends Controller
{
public function index()
{
return 'მთავარი გვერდი';
}
}
შევიტანოთ ცვლილებები routes/web.php ფაილში :
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\Admin;
use App\Http\Controllers\Admin\AdminsController;
use App\Http\Controllers\Admin\ContactsController;
use App\Http\Controllers\Admin\LoginController;
use App\Http\Controllers\Admin\ArticlesController;
use App\Http\Controllers\Front\IndexController;
Route::group(['prefix' => LaravelLocalization::setLocale(),'middleware' => [ 'localeSessionRedirect', 'localizationRedirect', 'localeViewPath' ]], function(){
// მთავარი გვერდი
Route::get('/', [IndexController::class, 'index'])->name('index');
});
Route::group(['middleware' => ['admin'], 'prefix' => 'admin'], function () {
// ავტორიზაცია და სისტემიდან გასვლა
Route::get('/login', [LoginController::class, 'showLogin'])->withoutMiddleware([Admin::class])->name('ShowLogin');
Route::post('/signin', [LoginController::class, 'login'])->withoutMiddleware([Admin::class])->name('AdminLogin');
Route::get('/logout', [LoginController::class, 'logout'])->name('AdminLogout');
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
// ადმინისტრატორები
Route::resource('admins', AdminsController::class);
// საკონტაქტო ინფორმაციის გვერდი
Route::resource('contacts', ContactsController::class, ['only' => ['edit','update']]);
Route::get('/contacts/cache', [ContactsController::class, 'cache'])->name('contacts.cache');
// სიახლეები
Route::resource('articles', ArticlesController::class);
});
თუ ახლა შევალთ პროექტის მთავარ გვერდზე, ვიხილავთ ტექსტს - 'მთავარი გვერდი'.
@yield('content')
ამ შაბლონის მემკვიდრე იქნება კლიენტის მხარის ყველა სხვა შაბლონი.
resources/views/front საქაღლდეშივე შევქმნათ მთავარი გვერდის შაბლონი index.blade.php შემდეგი კოდით :
@extends('front.layout')
@section('content')
მთავარი გვერდი
@endsection
ახლა ეს შაბლონი დავაბრუნებინოთ კონტროლერის მეთოდს :
namespace App\Http\Controllers\Front;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class IndexController extends Controller
{
public function index()
{
return view('front.index');
}
}
როგორც ადრეც ვთქვით, მომხმარების მხარისათვის გამოვიყენებთ ამ
შაბლონს.
შევქმნათ საქაღალდე public/assets/front და მასში ჩავაკოპიროთ ამ ბმულიდან გადმოწერილ საქაღალდეში არსებიული assets, js და css საქაღალდეები.
ახლა კი resources/views/front/layout.blade.php და resources/views/front/index.blade.php ფაილებში შევიტანოთ შემდეგი კოდები :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>@yield('title')</title>
<!-- Font Awesome icons (free version)-->
<script src="https://use.fontawesome.com/releases/v5.15.3/js/all.js" crossorigin="anonymous"></script>
<!-- Google fonts-->
<link href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css" />
<!-- Core theme CSS (includes Bootstrap)-->
<link href="{{ asset('assets/front/css/styles.css') }}" rel="stylesheet" />
</head>
<body>
<!-- მენიუ -->
<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
<div class="container px-4 px-lg-5">
<a class="navbar-brand" href="{{ route('index') }}">Start Bootstrap</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
Menu
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto py-4 py-lg-0">
<li class="nav-item">
<a class="nav-link px-lg-3 py-3 py-lg-4" href="
{{ route('index') }}">@lang('menu.index')
</a>
</li>
<li class="nav-item">
<a class="nav-link px-lg-3 py-3 py-lg-4" href="contact.html">
@lang('menu.contact')
</a>
</li>
<!-- ენების გადამრთველი -->
@foreach(LaravelLocalization::getSupportedLocales() as $localeCode => $properties)
<li class="nav-item">
<a href="{{ LaravelLocalization::getLocalizedURL($localeCode, null, [], true) }}" class="nav-link px-lg-3 py-3 py-lg-4">
{{ strtoupper(mb_substr($properties['name'], 0, 2)) }}
</a>
</li>
@endforeach
</ul>
</div>
</div>
</nav>
<!-- /მენიუ -->
<!-- საიტის ქუდი -->
<header class="masthead" style="background-image: url('{{ asset('assets/front/assets/img/home-bg.jpg') }}')">
<div class="container position-relative px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="site-heading">
<h1>Clean Blog</h1>
<span class="subheading">A Blog Theme by Start Bootstrap</span>
</div>
</div>
</div>
</div>
</header>
<!-- /საიტის ქუდი -->
<!-- ძირითადი შიგთავსი -->
@yield('content')
<!-- /ძირითადი შიგთავსი -->
<!-- საიტის ძირი -->
<footer class="border-top">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<ul class="list-inline text-center">
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-twitter fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-facebook-f fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
</ul>
<div class="small text-center text-muted fst-italic">Copyright © Your Website 2021</div>
</div>
</div>
</div>
</footer>
<!-- საიტის ძირი -->
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script src="{{ asset('assets/front/js/scripts.js') }}"></script>
</body>
</html>
@extends('front.layout')
@section('title', trans('menu.index'))
@section('content')
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<!-- Post preview-->
<div class="post-preview">
<a href="post.html">
<h2 class="post-title">Man must explore, and this is exploration at its greatest</h2>
<h3 class="post-subtitle">Problems look mighty small from 150 miles up</h3>
</a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on September 24, 2021
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
</div>
</div>
</div>
@endsection
<?php
return [
'index' => 'Home',
'contact' => 'Contact',
];
<?php
return [
'index' => 'მთავარი',
'contact' => 'კონტაქტი',
];
namespace App\Http\Controllers\Front;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App;
use App\Models\Article;
class IndexController extends Controller
{
public function index()
{
$articles = Article::all(App::getLocale()); // App::getLocale() მიმდინარე ენა
return view('front.index', compact('articles'));
}
}
როგორც ვხედავთ, მივმართეთ Article მოდელის all მეთოდს, რომელიც გამოიყურება შემდეგნაირად :
public static function all($local = null)
{
return Article::join('articles_translates', 'articles.id', '=', 'articles_translates.article_id')
->where('articles_translates.lang', $local)
->select('articles.*', 'articles_translates.title', 'articles_translates.description')
->orderBy('id', 'desc')
->get();
}
მეთოდში ბრძანებათა კონსტრუქტორის დახმარებით აღწერილი ჩანაწერი დააგენერირებს შემდეგ SQL ბრძანებას :
select articles.*, articles_translates.title, articles_translates.description
from articles
inner join articles_translates
on articles.id = articles_translates.article_id
where articles_translates.lang = 'ka'
order by id desc
ახლა ეს ინფორმაცია გამოვიტანოთ resources/views/front/index.blade.php ფაილში :
@extends('front.layout')
@section('title', trans('menu.index'))
@section('content')
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
@forelse($articles as $article)
<div class="post-preview">
<a href="post.html">
<h2 class="post-title">{{ $article->title }}</h2>
<h3 class="post-subtitle">{{ $article->description }}</h3>
</a>
<p class="post-meta">
@lang('site.author')
<a href="#!">Start Bootstrap</a>
@lang('site.date') : {{ $article->created_at }}
</p>
</div>
@if(!$loop->last)
<!-- დიზაინში არსებული გამყოფი ხაზი აღარაა საჭირო ბოლო სიახლის შემდეგ -->
<hr class="my-4" />
@endif
@empty
<div class="alert alert-danger">@lang('site.no_data')</div>
@endforelse
</div>
</div>
</div>
@endsection
სათარგმნი ფაილის (site.php) შექმნა უკვე ვიცით და ამიტომ აღარ დავკონკრეტდებით.
namespace App\Http\Controllers\Front;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App;
use App\Models\Article;
class IndexController extends Controller
{
public function index()
{
$articles = Article::all(App::getLocale());
return view('front.index', compact('articles'));
}
public function article($id)
{
$article = Article::item(App::getLocale(), $id);
if(!$article)
{
return redirect()->back();
}
return view('front.article', compact('article'));
}
}
როგორც ვხედავთ მივმართავთ Article მოდელის item მეთოდს, რომელსაც გადაეცემა ორი პარამეტრი
- სასურველი ენა და სასურველი ჩანაწერის იდენტიფიკატორი. ჩავამატოთ ეს მეთოდი მოდულში :
public static function item($local = null, $id = null)
{
return Article::join('articles_translates', 'articles.id', '=', 'articles_translates.article_id')
->where('articles.id', $id)
->where('articles_translates.lang', $local)
->select('articles.*', 'articles_translates.title','articles_translates.description','articles_translates.text')
->first();
}
მოთხოვნათა კონსტრუქტორის მეშვეობით დაგენერირდება შემდეგი SQL ბრძანება :
select articles.*, articles_translates.title, articles_translates.description, articles_translates.text
from articles
inner join articles_translates
on articles.id = articles_translates.article_id
where articles.id = 3 and articles_translates.lang = 'en'
limit 1
შემდეგ მიღებულ ინფორმაციას გადავცემთ წარმოდგენის ფაილს. შევქმნათ ფაილი resources/views/front/article.blade.php :
@extends('front.layout')
@section('title', $article->title)
@section('content')
<article class="mb-4">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
{!! $article->text !!}
</div>
</div>
</div>
</article>
@endsection
ახლა შევქმნათ შესაბამისი მარშრუტი :
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\Admin;
use App\Http\Controllers\Admin\AdminsController;
use App\Http\Controllers\Admin\ContactsController;
use App\Http\Controllers\Admin\LoginController;
use App\Http\Controllers\Admin\ArticlesController;
use App\Http\Controllers\Front\IndexController;
Route::group(['prefix' => LaravelLocalization::setLocale(),'middleware' => [ 'localeSessionRedirect', 'localizationRedirect', 'localeViewPath' ]], function(){
// მთავარი გვერდი
Route::get('/', [IndexController::class, 'index'])->name('index');
// სიახლის შიდა გვერდი
Route::get('/article/{id}', [IndexController::class, 'article'])->name('article');
});
Route::group(['middleware' => ['admin'], 'prefix' => 'admin'], function () {
// ავტორიზაცია და სისტემიდან გასვლა
Route::get('/login', [LoginController::class, 'showLogin'])->withoutMiddleware([Admin::class])->name('ShowLogin');
Route::post('/signin', [LoginController::class, 'login'])->withoutMiddleware([Admin::class])->name('AdminLogin');
Route::get('/logout', [LoginController::class, 'logout'])->name('AdminLogout');
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
// ადმინისტრატორები
Route::resource('admins', AdminsController::class);
// საკონტაქტო ინფორმაციის გვერდი
Route::resource('contacts', ContactsController::class, ['only' => ['edit','update']]);
Route::get('/contacts/cache', [ContactsController::class, 'cache'])->name('contacts.cache');
// სიახლეები
Route::resource('articles', ArticlesController::class);
});
გადმოწერილ Bootstrap შაბლონს თუ დავაკვირდებით ვნახავთ, რომ მთავარ გვერდზე header სექციას აქვს სტატიკური
ფონი (home-bg.jpg), ხოლო სიახლის შიდა გვერდს თუ გავხსნით ამ სექციას იქ უკვე სხვა ფონი აქვს, შევიტანოთ შესაბამისი
ცვლილებები მშობელ შაბლონში (resources/views/front/layout.blade.php) არსებულ header სექციაში :
@php
$articles_page = Route::current()->getName() == 'article' ? true : false;
@endphp
<header class="masthead" style="background-image: url('{{ $articles_page ? $article->image : asset('assets/front/assets/img/home-bg.jpg') }}')">
<div class="container position-relative px-4 px-lg-5">
<iv class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="site-heading">
<h1>{{ $articles_page ? $article->title : 'Clean Blog' }}
</div>
</div>
</div>
</div>
</header>
ანუ Route::current()->getName() ჩანაწერის მეშვეობით მოვახდინეთ იმის გადამოწმება, ვიმყოფებით თუ არა სიახლის
შიდა გვერდზე, თუ კი - მაშინ სექციას ფონად ედება სიახლის ფოტო, წინააღმდეგ შემთხვევაში კი სტატიკური სურათი. იგივე
ლოგიკით შევცვალეთ სექციაში არსებული ტექსტიც.
აქვე შევიტანოთ ცვლილებები მთავარ გვერდზე გამოტანილი სიახლეების ჩამონათვალის სექციაში, კერძოდ - ბმულები დავაკავშიროთ მარშრუტთან :
@extends('front.layout')
@section('title', trans('menu.index'))
@section('content')
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
@forelse($articles as $article)
<div class="post-preview">
<a href="{{ route('article', $article->id) }}">
<h2 class="post-title">{{ $article->title }}</h2>
<h3 class="post-subtitle">{{ $article->description }}</h3>
</a>
<p class="post-meta">
@lang('site.author')
<a href="{{ route('article', $article->id) }}">Start Bootstrap</a>
@lang('site.date') : {{ $article->created_at }}
</p>
</div>
@if(!$loop->last)
<!-- დიზაინში არსებული გამყოფი ხაზი აღარაა საჭირო ბოლო სიახლის შემდეგ -->
<hr class="my-4" />
@endif
@empty
<div class="alert alert-danger">@lang('site.no_data')</div>
@endforelse
</div>
</div>
</div>
@endsection
composer require laravel/breeze --dev
ამის შემდეგ გავუშვათ ეს ბრძანება :
php artisan breeze:install
როგორც ვიცით, ინსტალაციის შემდეგ იქმნება routes/auth.php ფაილი და ასევე ხდება ამ ფაილის routes/web.php
ფაილში გამოძახება :
php require __DIR__.'/auth.php';
თუმცა !
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\Admin;
use App\Http\Controllers\Admin\AdminsController;
use App\Http\Controllers\Admin\ContactsController;
use App\Http\Controllers\Admin\LoginController;
use App\Http\Controllers\Admin\ArticlesController;
use App\Http\Controllers\Front\IndexController;
Route::group(['prefix' => LaravelLocalization::setLocale(),'middleware' => [ 'localeSessionRedirect', 'localizationRedirect', 'localeViewPath' ]], function(){
// მთავარი გვერდი
Route::get('/', [IndexController::class, 'index'])->name('index');
// სიახლის შიდა გვერდი
Route::get('/article/{id}', [IndexController::class, 'article'])->name('article');
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');
});
Route::group(['middleware' => ['admin'], 'prefix' => 'admin'], function () {
// ავტორიზაცია და სისტემიდან გასვლა
Route::get('/login', [LoginController::class, 'showLogin'])->withoutMiddleware([Admin::class])->name('ShowLogin');
Route::post('/signin', [LoginController::class, 'login'])->withoutMiddleware([Admin::class])->name('AdminLogin');
Route::get('/logout', [LoginController::class, 'logout'])->name('AdminLogout');
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
// ადმინისტრატორები
Route::resource('admins', AdminsController::class);
// საკონტაქტო ინფორმაციის გვერდი
Route::resource('contacts', ContactsController::class, ['only' => ['edit','update']]);
Route::get('/contacts/cache', [ContactsController::class, 'cache'])->name('contacts.cache');
// სიახლეები
Route::resource('articles', ArticlesController::class);
});
require __DIR__.'/auth.php';
@extends('front.layout')
@section('title', trans('site.register'))
@section('content')
<div class="container">
@if($errors->any())
<div class="row">
<div class="col-md-4 offset-4">
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
@endif
<div class="row">
<div class="col-md-4 offset-md-4">
<form method="POST" action="{{ route('register') }}">
@csrf
<div class="form-group">
<label>@lang('site.name')</label>
<input type="text" name="name" value="{{ old('name') }}" class="form-control" required>
</div>
<div class="form-group">
<label>@lang('site.email')</label>
<input type="email" name="email" value="{{ old('email') }}" class="form-control" required>
</div>
<div class="form-group">
<label>@lang('site.password')</label>
<input type="password" name="password" class="form-control" required>
</div>
<div class="form-group">
<label>@lang('site.re_password')</label>
<input type="password" name="password_confirmation" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary mt-3 mb-3" style="width: 100%;">
@lang('site.register')
</button>
</form>
</div>
</div>
</div>
@endsection
რეგისტრაციისათვის უნდა ვესტუმროთ შემდეგ ბმულს : http://example.ge/register
რეგისტრაციის შემდეგ სისტემა გადაგვამისამართებს მომხმარებლის კაბინეტში, app/Http/Controllers/Auth/RegisteredUserController კონტროლერის store მეთოდი :
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(RouteServiceProvider::HOME);
}
app/Providers/RouteServiceProvider :
...
public const HOME = '/dashboard';
...
php artisan make:controller Front/UserController
ამ კონტროლერში სულ გვექნება სამი მეთოდი : პირადი კაბინეტის გამოსატანი მეთოდი, პირადი ინფორმაციის შეცვლის მეთოდი და
პაროლის განახლების მეთოდი. განვსაზღვროთ შესაბამისი მარშრუტები :
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\Admin;
use App\Http\Controllers\Admin\AdminsController;
use App\Http\Controllers\Admin\ContactsController;
use App\Http\Controllers\Admin\LoginController;
use App\Http\Controllers\Admin\ArticlesController;
use App\Http\Controllers\Front\IndexController;
use App\Http\Controllers\Front\UserController;
Route::group(['prefix' => LaravelLocalization::setLocale(),'middleware' => [ 'localeSessionRedirect', 'localizationRedirect', 'localeViewPath' ]], function(){
// მთავარი გვერდი
Route::get('/', [IndexController::class, 'index'])->name('index');
// სიახლის შიდა გვერდი
Route::get('/article/{id}', [IndexController::class, 'article'])->name('article');
Route::middleware(['auth'])->group(function () {
Route::get('/dashboard', [UserController::class, 'dashboard'])->name('dashboard');
Route::post('/update_data', [UserController::class, 'update_data'])->name('update_data');
Route::post('/update_password', [UserController::class, 'update_password'])->name('update_password');
});
});
Route::group(['middleware' => ['admin'], 'prefix' => 'admin'], function () {
// ავტორიზაცია და სისტემიდან გასვლა
Route::get('/login', [LoginController::class, 'showLogin'])->withoutMiddleware([Admin::class])->name('ShowLogin');
Route::post('/signin', [LoginController::class, 'login'])->withoutMiddleware([Admin::class])->name('AdminLogin');
Route::get('/logout', [LoginController::class, 'logout'])->name('AdminLogout');
// ადმინისტრატორის პანელის მთავარი გვერდი
Route::get('/', function () {
return view('admin.index');
})->name('AdminMainPage');
// ადმინისტრატორები
Route::resource('admins', AdminsController::class);
// საკონტაქტო ინფორმაციის გვერდი
Route::resource('contacts', ContactsController::class, ['only' => ['edit','update']]);
Route::get('/contacts/cache', [ContactsController::class, 'cache'])->name('contacts.cache');
// სიახლეები
Route::resource('articles', ArticlesController::class);
});
require __DIR__.'/auth.php';
resources/views/dashboard.blade.php მეთოდი გადავიტანოთ resources/views/front საქაღალდეში, ასევე
აღვწეროთ UserController კონტროლერის dashboard მეთოდი :
namespace App\Http\Controllers\Front;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function dashboard()
{
return view('front.dashboard');
}
}
თუ ახლა შევალთ http://127.0.0.1:8000/dashboard ბმულზე, ვიხილავთ მომხმარებლის, ასევე გაუსტილავ კაბინეტს.
გამოვასწოროთ ეს ხარვეზიც, resources/views/front/dashboard.blade.php ფაილში შევიტანოთ შემდეგი კოდი :
@extends('front.layout')
@section('title', trans('site.dashboard'))
@section('content')
<div class="container">
@if($errors->any())
<div class="row">
<div class="col-md-4 offset-4">
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
@endif
@if(Session::has('updating_results'))
<div class="row">
<div class="col-md-4 offset-4">
<div class="alert alert-{{ Session::get('updating_results')['class'] }}">
{{ Session::get('updating_results')['message'] }}
</div>
</div>
</div>
@endif
<div class="row">
<div class="col-md-4 offset-md-4">
<h2>@lang('site.personal_data')</h2>
<form method="POST" action="{{ route('update_data') }}">
@csrf
<div class="form-group">
<label>@lang('site.name')</label>
<input type="text" name="name" value="{{ Auth::user()->name }}" class="form-control" required>
</div>
<div class="form-group">
<label>@lang('site.email')</label>
<input type="email" name="email" value="{{ Auth::user()->email }}" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary mt-3 mb-3" style="width: 100%;">
@lang('site.update')
</button>
</form>
<h2>@lang('site.change_password')</h2>
<form method="POST" action="{{ route('update_password') }}">
@csrf
<div class="form-group">
<label>@lang('site.old_password')</label>
<input type="password" name="old_password" class="form-control" required>
</div>
<div class="form-group">
<label>@lang('site.new_password')</label>
<input type="password" name="new_password" class="form-control" required>
</div>
<div class="form-group">
<label>@lang('site.re_new_password')</label>
<input type="password" name="new_password_confirmation" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary mt-3 mb-3" style="width: 100%;">
@lang('site.update')
</button>
</form>
</div>
</div>
</div>
@endsection
UserController კონტროლერის მეთოდები კი იქნება ასეთი :
namespace App\Http\Controllers\Front;
use Hash;
use Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function dashboard()
{
return view('front.dashboard');
}
public function update_data(Request $request)
{
$this->validate($request, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email,' . Auth::user()->id,
]);
$update = Auth::user()->update([
'name' => $request->name,
'email' => $request->email
]);
if($update)
{
$request->session()->flash('updating_results', [
'class' => 'success',
'message' => trans('site.info_updated')
]);
}
else
{
$request->session()->flash('updating_results', [
'class' => 'danger',
'message' => trans('site.update_error')
]);
}
return redirect()->back();
}
public function update_password(Request $request)
{
// გადავამოწმოთ ემთხვევა თუ არა შეყვანილი ძველი პაროლი ავტორიზებული მომხმარებლის პაროლს
if(!Hash::check($request->old_password, Auth::user()->password))
{
$request->session()->flash('updating_results', [
'class' => 'danger',
'message' => trans('site.old_password_error')
]);
return redirect()->back();
}
$this->validate($request, [
'old_password' => 'required|string|min:8',
'new_password' => 'required|string|min:8|confirmed',
]);
$update = Auth::user()->update([
'password' => Hash::make($request->new_password)
]);
if($update)
{
$request->session()->flash('updating_results', [
'class' => 'success',
'message' => trans('site.info_updated')
]);
}
else
{
$request->session()->flash('updating_results', [
'class' => 'danger',
'message' => trans('site.update_error')
]);
}
return redirect()->back();
}
}
ამ კოდში ახალი და განსაკუთრებული არაფერია და ამიტომ დაკონკრეტებაზე აღარ დავკარგავ დროს.
@auth
<li class="nav-item">
<a href="{{ route('dashboard') }}" class="nav-link px-lg-3 py-3 py-lg-4">
@lang('site.dashboard')
</a>
</li>
<li class="nav-item">
<a href="#!"
class="nav-link px-lg-3 py-3 py-lg-4"
onclick="event.preventDefault(); document.getElementById('logout-form').submit();"
>
@lang('site.logout')
</a>
</li>
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
@csrf
</form>
@else
<li class="nav-item">
<a class="nav-link px-lg-3 py-3 py-lg-4" href="{{ route('register') }}">
@lang('site.register')
</a>
</li>
<li class="nav-item">
<a class="nav-link px-lg-3 py-3 py-lg-4" href="{{ route('login') }}">
@lang('site.login')
</a>
</li>
@endauth
როგორც ვხედავთ სისტემიდან გამოსასვლელად გამოვიყენეთ HTML ფორმა, რომელიც routes/auth ფაილში აღწერილი
logout მარშრუტის შესაბამისად აკითხავს app/Http/Controllers/Auth/AuthenticatedSessionController
კონტროლერის destroy მეთოდს :
public function destroy(Request $request)
{
Auth::guard('web')->log