Laravel 10.*



1. რა არის Laravel ?

Laravel - MVC პრინციპზე დაფუძნებული პლატფორმა, ფრეიმვორკი, ინსტრუმენტთა ნაკრები იმ პროგრამისტებისათვის, რომლებიც PHP-ის მეშვეობით ამზადებენ ვებ-გვერდებს. ფრეიმვორკის დახმარებით გაცილებით სწრაფად და მარტივად ვქმნით აპლიკაციებს და აღარ გვიწევს კოდის წერის დაწყება ნოლიდან. ფაქტიურად ფრეიმვორკი წარმოადგენს მომავალი პროექტის კარკასს, ჩონჩხს, მასში ჩადებულია ისეთი კლასები რომელთა მეშვეობითაც პროგრამისტს აღარ უწევს მთელი რიგი სტანდარტული და რუტინული სამუშაოების ჩატარება, მაგალითად: საიტზე შესვლის წერტილის შექმნა, ადამიანისათვის გასაგები URL-ების შექმნა, შემავალი პარამეტრებისა და მონაცემების ვალიდაციის მექანიზმის შექმნა, მონაცემთა ბაზასთან მუშაობის ორგანიზება და ა.შ

Laravel-ის პირველი ვერსია შეიქმნა 2011 წელს (ავტ. Taylor Otwell).

2. ინსტალაცია
Laravel-თან სამუშაოდ დაგვჭირდება : რა თქმა უნდა ბრაუზერი, რომელიმე ტექსტ-ედიტორი ან IDE (Integrated Development Environment - პროგრამული უზრუნველყოფა, რომელიც გამოიყენება აპლიკაციების შექმნისას და რომელიც ერთიან გრაფიკულ ინტერფეისში (GUI - graphical user interface) აერთიანებს პროგრამისტისათვის საჭირო სხვადასხვა ხელსაწყოებსა და ინსტრუმენტებს. მაგ: NetBeans, PhpStorm ...), PHP ინტერპრეტატორი და მონაცემთა ბაზის სერვერი.
Laravel 10.x ვერსიის ინსტალაციისათვის დაგვჭირდება PHP-ის მინიმუმ 8.1 ვერსია.

დიდი ალბათობით, Laravel-ის შესწავლისას მომხმარებელს უკვე ექნება შეხება PHP-ს რომელიმე ინტერპრეტატორთან და მონაცემთა ბაზის სერვერთან, ეს შეიძლება იყოს როგორც ცალკე დაინსტალირებული პროგრამები, მაგ: ვებ-სერვერი Apache, PHP ინტერპრეტატორი, მბ სერვერი MYSQL, ასევე გამზადებული ნაკრებები მაგ: Denver, OpenServer, XAMPP, WAMP, LAMP და ა.შ. შესაძლებელია მათი გამოყენებაც.

ინსტალაცია Composer-ით

რა არის Composer-ი ?

Composer-ი არის სხვადასხვა დამოკიდებულებების მენეჯერი, რომლის საშუალებითაც ხდება, ამა თუ იმ ბიბლიოთეკების, დამოკიდებულებებისა და პაკეტების ინტეგრირება PHP პროგრამულ უზრუნველყოფაში. Windows ოპერაციულ სისტემაში Composer-ის ინსტალაცია საკმაოდ მარტივია, უბრალოდ უნდა გადმოვწეროთ საინსტალაციო ფაილი და დავაინსტალიროთ (ავტორები: Nils Adermann და Jordi Boggiano).

ინტალაციის პროცესი მოგვთხოვს გზას PHP-მდე, რომელიც შეიძლება გამოიყურებოდეს ასე:





ინსტალაციის შემდეგ ბრძანებათა ველიდან გავუშვათ შემდეგი ბრძანება : composer თუ შედეგად ვიხილავ ამდაგვარ სურათს:



ესეიგი ყველაფერი რიგზეა.

ახალი პროექტის შექმნა

ახალი პროექტის შესაქმნელად გამოიყენება composer-ის ბრძანება create-project, ბრძანებასთან ერთად ასევე უნდა მივუთითოთ სასურველი პაკეტის დასახელება და იმ დირექტორიის დასახელება სადაც გვსურს, რომ შეიქმნას ახალი პროექტი: composer create-project <PACKAGE_NAME> <MY_PROJECT>

პაკეტების საცავი (Repository)

ამა თუ იმ დამოკიდებულების ან პაკეტის ინსტალაციის პროცესს თუ მარტივი ენით განვმარტავთ, მივიღებთ ასეთ სურათს : მას შემდეგ რაც გაეშვება ინსტალაციის ბრძანება, Composer-ი მიაკითხავს რაღაც წყაროს, რესურსს, სადაც განთავსებულია სასურველი პაკეტის შესაბაისი ფაილები, დამოკიდებულებები და ა.შ და ამ ყველაფერს გადმოწერს ჩვენს ლოკალურ სისტემაში.

ჩნდება კითხვები : მაინც სად არის ინახება ეს პაკეტები ? საიდან მოაქვს Composer-ს ეს ყველაფერი ?

როდესაც Composer-ი ინსტალირდება, ნაგულისხმეობის პრინციპით ხდება ერთადერთი საცავის რეგისტრაცია, ეს საცავია ვებ-გვერდი Packagist.org.

***

მაშ ასე, ბრძანებათა ველის მეშვეობით გადავდივართ სასურველ დირექტორიაში და ვუშვებთ შემდეგ ბრძანებას :
composer create-project laravel/laravel laravel   
            
ბრძანების გაშვების შემდეგ დაიწყება Laravel-ის ინსტალაცია და აგრეთვე შეიქმნება laravel საქაღალდე შესაბამისი ფაილებისა და საქაღალდეების სტრუქტურით :



როგორ გავხსნათ პროექტი ?

რა არის Artisan ?

ინგ: Artisan - ხელოსანი, ოსტატი.

Artisan-ი არის Laravel-ში ჩადგმული ბრძანებათა ინტერფეისის სახელწოდება, რომლის მეშვეობითაც შესაძლებელია სხვადასხვა საჭირო ბრძანებებისა და ინსტრუქციების საკმაოდ მარტივად გაშვება აპლიკაციაზე მუშობის პროცესში. Artisan ბრძანებათა სრული ჩამონათვალის სანახავად უნდა გავუშვათ შემდეგი ბრძანება :

php artisan list

***

ბრძანებათა ველიდან გადავიდეთ ახლად შექმნილ საქაღალდეში - laravel და ავამუშავოთ პროექტი შემდეგი ბრძანების დახმარებით : php artisan serve შედეგი იქნება :



თუ ახლა შევალთ ამ მისამართზე: http://127.0.0.1:8000, ვიხილავთ ჩვენს პროექტს :



3. რა არის MVC ?
MVC (Model–View–Controller ანუ მოდელი-წარმოდგენა-კონტროლერი) - არის კომპიუტერულ ინჟინერიაში გავრცელებული, კოდირების შაბლონი, მეთოდი, იდეა, რომელიც საშუალებას იძლევა განცალკევდეს ვებ-აპლიკაციის ლოგიკა და მისი წარმოდგენა ანუ ის ნაწილი რომელსაც მომხმარებელი ხედავს ბრაუზერში.



ეს განცალკევება აპლიკაციას ხდის უფრო მოქნილს და ადვილად კორექტირებადს, რადგან თუ გვსურს ვიზუალური მხარის ჩასწორება საქმე გვაქვს მხოლოდ მასთან და არა ლოგიკურ ფუნქციონალთან და პირიქით.

წარმოდგენა

MVC-ს ელემენტ - 'წარმოდგენა (view)'-ს ინფორმაცია გამოაქვს ეკრანზე. ეს არის აპლიკაციის დიზაინერული ნაწილი მინიმალური ლოგიკით. წარმოდგენა შეიძლება შედგებოდეს რამდენიმე შაბლონისაგან.



კონტროლერი

MVC-ს ელემენტი - 'კონტროლერი' არის მომხმარებლისა და აპლიკაციის წარმოდგენის დამაკავშირებელი ბლოკი. იგი იღებს ინფორმაციას მომხმარებლისაგან, ამუშავებს, ამოწმებს მას და ეს დამუშავებული ინფორმაცია ეგზავნება MVC-ს მესამე ელემენტს - 'მოდელს', კონტროლერი მისგან ღებულობს შესაბამის პასუხს და წყვეტს თუ წარმოდგენის რომელმა შაბლონმა უნდა დაუბრუნოს ეს პასუხი მომხმარებელს.



მოდელი

MVC-ს მესამე ელემენტში - 'მოდელი', ხდება სისტემის მუშაობის ლოგიკის ძირითადი ნაწილის აღწერა. მოდელი მუშაობს მონაცემებთან, ახდენს მონაცემებით მანიპულირებას. მოდელი იღებს კონტროლერისაგან რაიმე მოთხოვნას, შემდეგ მბ-დან მოაქვს შესაბამისი ინფორმაცია და უბრუნებს კონტროლერს. სხვა სიტყვებით, რომ ვთქვათ მოდელის ფუნქციაა მიიღოს მონაცემი, დაამუშავოს იგი და დააბრუნოს პასუხი. პრაქტიკაში მოდელის რეალიზება ხშირად ხდება ხოლმე კლასის სახით, რომელსაც აქვს მონაცეთა დამუშავებისათვის განსაზღვრული მეთოდები.



მუშაობის კლასიკური სქემა

მომხმარებელმა გააკეთა მოთხოვნა საიტზე, მოთხოვნა გადაეცა საიტზე შესვლის ერთადერთ წერტილს (index.php, frontController), ამ წერტილში მოსულ მოთხოვნას ამუშავებს მარშრუტიზაციის კომპონენტი (Router), ერთგვარი გზამკვლევი მოთხოვნისათვის, როუტერმა უნდა განსაზღვროს თუ რომელმა კონტროლერმა უნდა დაამუშავოს მოთხოვნა



ვთქვათ ეს კონტროლერია NewsController, ეს კონტროლერი მოთხოვნიდან ღებულობს ინფორმაციას თუ რა სახის სიახლეების ნახვა უნდა მომხმარებელს, შემდეგ მიმართავს მოდელს საჭირო ინფორმაციის მისაღებად, პასუხის მიღების შემდეგ კონტროლერი იყენებს საჭირო წარმოდგენას პასუხის ბრაუზერში გამოსატანად, ასე იკვრება მთლიანი წრე.
4. ფრეიმვორკის სტრუქტურა

დირექტორიები

App დირექტორია

App დირექტორია მოიცავს ჩვენი აპლიკაციის ძირითად კოდს, მასშია მოთავსებული აპლიკაციის კლასები. ამ საქაღალდეს უფრო დაწვრილებით განვიხილავთ ოდნავ მოგვიანებით.

Bootstrap დირექტორია

ამ საქაღალდის დასახელებას საერთო არაფერი აქვს ფრეიმვორკ Bootstrap-თან :)) აქ თავმოყრილია ფაილები, რომლებიც ახორციელებენ ფრეიმვორკ Laravel-ის თავდაპირველ ჩატვირთვას და ახდენენ კლასების ავტოჩატვირთვას (ინგლ: Bootstrap - ახალი ვარიანტის შესაქმნელად სისტემის ან პროცესის არსებული ვარიანტის გამოყენება. Bootstrapping - თვითრეგულირება, თვითაწყობა, თვითჩატვირთვა, თვითუზრუნველყოფა).

ამ საქაღალდეში აგრეთვე მოთავსებულია კატალოგი cache, სადაც თავმოყრილი ფაილები გამოიყენება ფრეიმვორკის მუშაობის დროს მიმდინარე პროცესების ოპტიმიზაციისათვის (მაგ: მარშრუტიზაცია, ფაილთა ქეშირება)

Config დირექტორია

ეს საქაღალდე, როგორც მისივე დასახელებიდანაც ჩანს, შეიცავს აპლიკაციის კონფიგურაციულ ფაილებს. სასურველია მომხმარებელი გაეცნოს ამ ფაილებს და შეისწავლოს თუ რომელ მათგანში რა არის განსაზღვრული.

Database დირექტორია

ეს საქაღალდე, როგორც მისივე დასახელებიდანაც ჩანს, შეიცავს მონაცემთა ბაზასთან დაკავშირებულ ფაილებსა და საქაღალდეებს, აქ ხდება მბ-ს პარამეტრების განსაზღვრა (ჰოსტი, მომხმარებელი, პაროლი, მბ-ს სახელი და ა.შ).

Public დირექტორია

ეს დირექტორია შეიცავს index.php ფაილს, რომელიც არის ჩვენ აპლიკაციაში შესასვლელი წერტილი, დირექტორია აგრეთვე შეიცავს ისეთ ხელსაწყო-ატრიბუტებს როგორებიცაა სურათები, ჯავასკრიპტის ფაილები, CSS ფაილები...

Resources დირექტორია

ეს საქაღალდე შეიცავს წარმოდგენის ფაილებს (views), არაკომპილირებად ინსტრუმენტებს როგორებიცაა LESS, SASS ან JavaScript. აქვეა მოთავსებული თარგმანებთან დაკავშირებული ფაილებიც.

Routes დირექტორია

ეს საქაღალდე შეიცავს აპლიკაციის მარშრუტების განმსაზღვრელ ფაილებს, ნაგულისხმეობის პრინციპით Laravel-ში ჩართულია მარშრუტთა რამდენიმე ფაილი :web.php, api.php, console.php და channels.php.

Storage დირექტორია

ეს დირექტორია შეიცავს კომპილირებულ Blade შაბლონებს, ფაილებზე დაფუძნებულ სესიებს, ქეშ-ფაილებს და ფრეიმვორკის ფარგლებში შექმნილ სხვა ფაილებს. საქაღალდე იყოფა app, framework და logs საქაღალდეებად.

Vendor დირექტორია

ეს დირექტორია შეიცავს Composer-ის დამოკიდებულებებს.

App დირექტორია დაწვრილებით

როგორც აღვნიშნეთ, აპლიკაციის ძირითადი ნაწილი განთავსებულია აქ. მას ავტომატურად ტვირთავს Composer-ი PSR-4 (PHP Standard Recommendation) ავტოჩატვირთვის სტანდარტით. ამ საქაღალდეს App დასახელება ენიჭება ნაგულისმეობის პრინციპით, სახელის გადარქმევა კი შესაძლებელია შემდეგი ბრძანებით php artisan app:name <name-of-your-application>

Console დირექტორია

აქ მოთავსებულია სამომხმარებლო Artisan ბრძანებები. ამ ბრძანებების გენერირება ხდება make:command ბრძანებით.

Exceptions დირექტორია

შეიცავს აპლიკაციაში დაფიქსირებულ გამონაკლისთა დამმუშავებლებს, თუ გვსურს რომ განვსაზღვროთ თუ როგორ დარეგისტრირდეს ან გამოისახოს კონკრეტული გამონაკლისი, შესაბამისად უნდა დავარედაქტიროთ, ამ საქაღალდეში არსებული Handler კლასი.

Http დირექტორია

შეიცავს კონტროლერებს, ფორმის მოთხოვნებს. აქვე თავსდება აპლიკაციისაკენ მიმართული მოთხოვნების დამუშავების თითქმის მთლიანი ლოგიკა.

Providers დირექტორია

ეს დირექტორია შეიცავს ჩვენი აპლიკაციის სერვის-პროვაიდერებს.

ზემოთ ჩამოთვლილ თითოეულ საქაღალდესა და დირექტორიას შევეხებით სწავლების პროცესში, კონკრეტული ამოცანების გადაჭრისას.
5. ფრეიმვორკის კონფიგურაციის გამართვა
ფრეიმვორკ Laravel-ს, ინსტალაციის შემდეგ თითქმის არ ჭირდება დამატებით რაიმე კონფიგურაციული პარამეტრების განსაზღვრა, თუმცა არსებობს გამონაკლისებიც, მაგალითად მონაცემთა ბაზასთან დასაკავშირებელი პარამეტრები. ასევე შეიძლება დაგვჭირდეს დროის სარტყელის მითითებაც და ა.შ. როგორც უკვე ვიცით, კორფიგურაციული პარამეტრების განსაზღვრა ხდება config დირექტორიაში არსებული ფაილების მეშვეობით.

პროექტის კეთების ეტაპი შესაძლებელია დავყოთ სამ ნაწილად:

  • პირველ ეტაპზე ვმუშაობთ ლოკალურ სივრცეში, ეს შეიძლება იყოს OpenServer, Virtualbox, XAMPP...
  • მეორე ეტაპზე ხდება პროექტის ტესტირება, სავარაუდო შეცდომების გასწორება...
  • მესამე ეტაპზე კი პროექტი იტვირთება უკვე რეალურ ჰოსტინგზე ანუ ეშვება სამუშაო გარემოში
შევთანხმდეთ, რომ სამივე ეტაპზე, კონფიგურაციული პარამეტრების სხვადასხვაგვარად განსაზღვრაა საჭირო , მაგალითად მბ-სთან დასაკავშირებელი პარამეტრები სულ სხვაა ლოკალურ სივრცეში და სულ სხვა იქნება რეალურ სერვერზე. ამიტომ იმ პარამეტრთა ჯგუფის მიხედვით, რომელთა შეცვლაც საჭიროა მუშაობის პროცესის ეტაპებიდან გამომდინარე, შეიძლება მოვახდინოთ კონფიგურაციული პარამეტრების ერთგვარი არეების, გარემოების ფორმირება. ამ გარემოში შემავალ პარამეტრებს ეწოდებათ გარემოს ცვლადები , რომლებიც, 'გასაღები=მნიშვნელობა' წყვილების სახით ინახება .env ფაილში (ინგ: Environment - გარემოცვა; გარემო). ამ ფაილის სტრუქტურა შემდეგნაირია :

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}"
            
  • APP_NAME - აპლიკაციის დასახელება.

  • APP_ENV - ანუ სამუშაო გარემო (Application Environment), აპლიკაციის კეთების პროცესის მიმდინარე ფაზა, ეტაპი, მასზეა დამოკიდებული ყველა სხვა პარამეტრის მნიშვნელობა.

  • APP_KEY - ამ პარამეტრში შენახულია შემთხვევითი სტრიქონი, რომელიც გამოიყენება ფრეიმვორკის მუშაობის პროცესში, სხვადასხვა მონაცემების დასაშიფრად. APP_KEY-ს მნიშვნელობა ცნობილია მხოლოდ ჩვენი აპლიკაციის შიგნით და არსად სხვაგან. შესაბამისად მხოლოდ ჩვენი აპლიკაციისთვისაა შესაძლებელი, ორმხრივი ალგორითმებისა და ამ გასაღების მეშვეობით დაშიფრული ინფორმაციის დეშიფრაცია.

    სად გამოიყენება APP_KEY ?

    გამოიყენება ფრეიმვორკის ნებისმიერ ადგილას სადაც საჭიროა მონაცემების შიფრაცია ორმხრივი ალგორითმების დახმარებით (მაგ: Sessions, CSRF tokens, Cookies)

    სად არ გამოიყენება APP_KEY ?

    არ გამოიყენება იმ ადგილას სადაც ხდება მონაცემების ჰეშირება ცალმხრივი ალგორითმების დახმარებით (მაგ: Passwords, password_reset_token)

  • APP_DEBUG - შეცდომების გამოტანის პარამეტრი, თუ მითითებულია მნიშვნელობა true მაშინ სისტემა გამოიტანს დაფიქსირებული შეცდომის შესახებ შეტყობინებას, ხოლო თუ მითითებულია false მაშინ არ გამოიტანს. პროექტის შექმნის ეტაპზე სასურველია ეს პარამეტრი იყოს ჩართული, ხოლო სამუშაო გარემოში კი - გამორთული.

  • DB განყოფილება - მოიცავს მონაცემთა ბაზასთან სამუშაოდ აუცილებელ პარამეტრებს.

  • CACHE_DRIVER - ქეშირების სისტემის პარამეტი.

  • SESSION_DRIVER - სესიებთან სამუშაო პარამეტრი.

  • REDIS განყოფილება - REDIS-თან სამუშაო პარამეტრები. (REDIS - remote dictionary server : «გასაღები — მნიშვნელობა» ფორმატის ინფორმაციის შესანახი საცავი, მონაცემთა ბაზის მართვის სისტემა ღია წყაროთი.)

  • MAIL განყოფილება -ელ_ფოსტასთან დაკავშირებული პარამეტრები.

მუშაობა გარემოს ცვლადებთან

გარემოს ცვლადების მნიშვნელობები მოქცეულია სუპერგლობალურ ცვლადში - $_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/, ვიხილავთ შემდეგ სურათს :



config საქაღალდე

app.php ფაილი

ეს ფაილი მოიცავს ფრეიმვორკის მუშაობისთვის აუცილებელ გლობალურ პარამეტრებს. თუ მას გავხსნით შევამჩნევთ, რომ ბრუნდება ასოციაციური მასივი, მისი პირველი გასაღები არის "name" '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, მასში შეტანილია ფასადების ანუ სისტემური კლასების ფსევდონიმები. თუ რა არის ფასადი და როგორ ვიმუშაოთ მასთან, ცოტა მოგვიანებით განვმარტავთ.

database.php ფაილი

ეს ფაილი გამოიყენება მონაცემთა ბაზასთან წვდომისათვის. ამ ფაილშიც ბრუნდება ასოციაციური მასივი, რომლის ერთ-ერთი გასაღები არის: 'default' => env('DB_CONNECTION', 'mysql') ანუ მონაცემთა ბაზის სამართავი ნაგულისმები სისტემა, შევამჩნიოთ რომ ამ გასაღების მნიშვნელობაც .env ფაილიდან მოდის env ფუნქციის მეშვეობით, მაგრამ იქ მონაცემთა ბაზის სამართავი სისტემის დასახელება განსაზღვრული ჯერჯერობით არ გვაქვს, ამიტომ ან უნდა განვსაზღროთ იგი .env ფაილში DB_CONNECTION=mysql ანდა არ განვსაზღვროთ და სისტემა გამოიყენებს ნაგულისმებ მნიშვნელობას, რომელიც env ფუნქციას მეორე პარამეტრად აქვს გადაცემული.

წვდომა კონფიგურაციულ პარამეტრებთან

გლობალური დამხმარე ფუნქციის - config-ის მეშვეობით, შესაძლებელია მივწვდეთ კონფიგურაციულ პარამეტრებს ჩვენი აპლიკაციის ნებისმიერი წერტილიდან. ეს ხდება 'კონფიგურაციული_ფაილის_დასახელება.პარამეტრი' სინტაქტსის მეშვეობით. შესაძლებელია ფუნქციას გადაეცეს ნაგულისმები მნიშვნელობაც იმ შემთხვევისათვის თუ შესაბამის ფაილში შესაბამისი კონფიგურაციული პარამეტრი ვერ მოიძებნება :
$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 Mode)

config/app.php ფაილში აღწერილი debug პარამეტრი განსაზღვრავს თუ რა სახით მიიღოს ინფორმაცია მომხმარებელმა რაიმე ხარვეზის ან შეცდომის დაფიქსირების მომენტში. ნაგულისმეობის პრინციპით ამ პარამეტრის მნიშვნელობა ასეა აღწერილი : 'debug' => (bool) env('APP_DEBUG', false),
სატესტო გარემოში ჩვენივე კომფორტისათვის ჯობია, რომ ამ პარამეტრის მნიშვნელობა იყოს true ანუ შეცდომების აღმოფხვრის რეჟიმი იყოს ჩართული. რეალურ გარემოში მისი ჩართვა კი დაუშვებელია, რადგან შეცდომის შესახებ ინფორმაციის გამოტანისას ეკრანზე გამოჩნდება ისეთი კონფიგურაციული პარამეტრების მნიშვნელობები, რომელთა სხვებისათვის ჩვენებაც დაუშვებელია.
6. არქიტექტორული კონცეფციები

მოთხოვნის დამუშავების სქემა

მომხმარებლის მიერ გაკეთებული ნებისმიერი მოთხოვნა მიემართება პროექტის ერთადერთი გლობალური შესავალი წერტილისაკენ - public/index.php ფაილისაკენ, ამის შემდეგ ეს ფაილი ტვირთავს Composer-ის მიერ შექმნილ კლასთა ავტოჩამტვირთველ vendor/autoload.php ფაილს 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,
];

...
            

ამ მასივში აღწერილია ის შუამავლები, რომლებიც აუცილებლად უნდა გაიაროს მომხმარებლის მიერ გაკეთებულმა მოთხოვნამ (შუამავლების შესახებ ვისაუბრებთ ოდნავ მოგვიანებით).

ავტოჩატვირთვისა და ყველა საჭირო პროცესის ინიციალიზაციის შემდეგ მოთხოვნა გადაეცემა მარშრუტიზატორს და ფრეიმვორკი ღებულობს გადაწყვეტილებას თუ რომელმა მარშრუტმა უნდა დაამუშავოს ესა თუ ის მოთხოვნა. მარშრუტმა მოთხოვნა შეიძლება გადასცეს კონკრეტულ კონტროლერს ან უბრალოდ ფუნქციას, თუ მოთხოვნა კონტროლერს გადაეცემა, იგი ჩაატარებს შესაბამის სამუშაოებს (ინფორმაციის გადამოწმება, ვალიდაცია და ა.შ) და თავის მხრივ მიმართავს მოდელს, თუ აუცილებელია მოდელი მიმართავს მონაცემთა ბაზას , ბაზა დაუბრუნებს მას პასუხს, მოდელი ამ პასუხს დაუბრუნებს კონტროლერს, კონტროლერი კი მიღებულ პასუხს გადასცემს წარმოდგენის შაბლონს, წარმოდგენის შაბლონი უზრუნველჰყოფს შედეგის ბრაუზერში გამოტანას. ხოლო თუ მარშრუტიზატორი მოთხოვნას არ გადასცემს კონტროლერს, არამედ გადასცემს ფუნქციას, მაშინ ეს ფუნქცია დაამუშავებს მოთხოვნას და შედეგს გადასცემს პირდაპირ წარმოდგენას.

სერვისების კონტეინერი

რა არის სერვისი ?

მარტივად თუ ვიტყვით სერვისის უკან მოიაზრება ფუნქციონალი რომელიც გვიმარტივებს ცხოვრებას :)) როდესაც Laravel-ს დავაინსტალირებთ ჩვენ უკვე გვექნება საკმაოდ ბევრი მზა ასეთი სერვისი: მონაცემთა ბაზასთან სამუშაო სერვისი, ფაილებთან სამუშაო სერვისი და ა.შ, მაგრამ შეიძლება გადავაწყდეთ ისეთი ამოცანებს, რომელთა გადასაჭრელი გზები, ფრეიმვორკის საბაზისო კომპლექტაციაში გათვალისწინებული არ იყოს, ანუ დაგვჭირდეს ჩვენი საკუთარი, სამომხმარებლო ფუნქციონალის ჩამატება სისტემაში. შეგვიძლია ამ ფუნქციონალსაც სერვისები ვუწოდოთ.

რას ნიშნავს დამოკიდებულებათა ინექცია (DI - Dependency injection) ?

ინგ: Injection - ინექცია; დანერგვა; შემოღება; ჩადება;

თუ კონკრეტული კლასის მუშაობისათვის საჭიროა, გამოყენებულ იქნას სხვა კლასის ფუნქციონალი, ეს იმას ნიშნავს, რომ პირველი კლასი დამოკიდებულია მეორე კლასზე. ბუნებრივია უნდა მოვახდინოთ დამხმარე კლასის საწყის კლასში ინტეგრირება (Injection) და მხოლოდ ამის შემდეგ შეგვეძლება დამხმარე კლასის ფუნქციონალის გამოყენება. სწორედ ამ დამხმარე კლასს ეწოდება დამოკიდებულება (Dependency).
...

use Illuminate\Http\Request;

...

class SomeController extends Controller
{
    ...

    public function post(Request $request)
    {
        $request->validate([
            // ...
        ]);

        // ... 
    }

    ...
}
            
ამ მაგალითზე შეიძლება ითქვას, რომ SomeController კლასში მოვახდინეთ Request დამოკიდებულების ინექცია.

რა არის სერვისების კონტეინერი ?

სერვისების კონტეინერი (იგივე დამოკიდებულებათა ინექციების კონტეინერი) არის ერთგვარი სივრცე, გარემო, რომლის დახმარებითაც ხდება კლასებს შორის ურთიერთდამოკიდებულებების მართვა და ამ დამოკიდებულებების სხვადასხვა ადგილას გამოყენება. სერვისების კონტეინერთან წვდომა შესაძლებელია app() ჩანაწერით:
Route::get('/', function(){
    
    echo '<pre>';
    print_r(app());
    echo '</pre>';
    die;    
    
});
            
თუ ახლა აპლიკაციის მთავარ გვერდზე შევალთ, ვიხილავთ ამდაგვარ სურათს :



როგორც ვხედავთ, სერვისების კონტეინერი არის Illuminate\Foundation\Application კლასის ეგზემპლიარი და ინახავს ინფორმაციას ხელმისაწვდომი ანუ დარეგისტრირებული სერვისების შესახებ. Illuminate\Foundation\Application კლასი კი, თავის მხრივ, არის Illuminate\Container\Container.php კლასის ანუ, ძირითადი კონტეინერული კლასის მემკვიდრე.

საკუთარი სერვისის შექმნა

შევქმნათ საქაღალდე app/Services და მასში კი სერვისი - MathService. სერვისში აღვწეროთ მარტივი მეთოდი, რომელიც დაგვიბრუნებს მასივის ელემენტების ჯამს :
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 ეს იმას ნიშნავს, რომ სისტემამ ვერ იპოვა ის დამოკიდებულება, რომელიც მოვთხოვეთ. სწორედ ამ პრობლემის მოგვარებაში დაგვეხმარება სერვისისების კონტეინერი და სერვისის პროვაიდერი.

რა არის სერვისის პროვაიდერი ?

სერვისების პროვაიდერებს უჭირავთ ცენტრალური ადგილი Laravel-ის არქიტექტურულ სტრუქტურაში. მათი მეშვეობით ხდება აპლიკაციის საწყისი ჩატვირთვა, უფრო კონკრეტულად - პროვაიდერები ტვირთავენ საჭირო ელემენტებსა და სერვისებს, მოვლენათა დამმუშავებლებს (event listeners), შუამავლებს და ა.შ. თუ გავხსნით config/app.php ფაილს, ვნახავთ მასში აღწერილი მასივის providers ველს, სწორედ ამ ველში შეტანილი პროვაიდერების ჩატვირთვა ხდება აპლიკაციის გაშვებისას. ეს ჩანაწერები ამატებენ სხვადასხვა ფუნქციონალს სერვისების კონტეინერში.

ნებისმიერი პროვაიდერი არის Illuminate\Support\ServiceProvider კლასის მემკვიდრე და შეიცავს ორ მეთოდს : register და boot. როდესაც აპლიკაცია იტვირთება, სისტემა აკითხავს ყველა არსებული პროვაიდერის register მეთოდს და ასრულებს თითოეულ მათგანში აღწერილ ინსტრუქციებს. პროვაიდერის შექმნა შესაძლებელია შემდეგი ბრძანების მეშვეობით :

php artisan make:provider MathServiceProvider
            

register მეთოდი

პროვაიდერის register მეთოდში ხდება სერვისების რეგისტრაცია ანუ შენახვა სერვისების კონტეინერში, კონტეინერთან წვდომა შესაძლებელია app თვისების მეშვეობით, ხოლო უშუალოდ ინფორმაციის მიმაგრება სერვისების კონტეინერის bind მეთოდით :
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) {
        //
    });
}

...
            

boot მეთოდი

იმ მომენტში, როდესაც მოხდება წარმოდგენის ფაილების გენერირება და ჩატვირთვა, შესაძლებელია დაგვჭირდეს რაიმე ფუნქციონალის შესრულება (მაგალითად წარმოდგენის შაბლონს გადავცეთ რაიმე ინფორმაცია). ასეთი ოპერაციების აღწერა ხდება პროვაიდერის boot მეთოდში :
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 () {
            //
        });
    }
}
            
წარმოდგენის ფაილებზე უფრო დაწვრილებით ვისაუბრებთ მოგვიანებით.

ფასადები

ფასადი არის სტატიკური ინტერფეისი სერვისების კონტეინერში დარეგისტრირებული კლასებისათვის, რომლის მეშვეობითაც საკმაოდ მარტივად ხდება ამ კლასებთან წვდომა. Laravel-ის ფასადები განთავსებულია vendor/laravel/framework/src/Illuminate/Support/Facades საქაღალდეში, თითოეული მათგანი არის, ამავე საქაღალდეში არსებულ - Facade.php ფაილში აღწერილი აბსტრაქტული კლასის მემკვიდრე და ატარებს Illuminate\Support\Facades სახელსივრცეს (namespace).
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;

Route::get('/cache', function () {
    return Cache::get('key');
});
            

საკუთარი ფასადის შექმნა

პირველ რიგში გავამარტივოთ ჩვენი App/Services/MathService.php სერვის, იგი გადავაკეთოთ ამდაგვარად :
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()
    {
        //
    }
}
            

მაგიური მეთოდები __call() და __callStatic

PHP ში __call() მეთოდი გამოიყენება, როდესაც მივმართავთ კლასის არარსებულ ან დაცულ მეთოდს, __callStatic() მეთოდი კი გამოიყენება მაშინ, როდესაც მივმართავთ კლასის არარსებულ ან დაცულ სტატიკურ მეთოდს. ორივე მათგანს გადაეცემა ორი პარამეტრი: მეთოდის დასახელება და არგუმენტები : 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' სტატიკურ კონტექსტში

***

მშობელ კლას - Facade-ში აღწრილია მაგიური მეთოდი __callStatic :
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
    }
}
            
7. მარშრუტები
მანამ სანამ ფრეიმვორკ Laravel-ის მარშრუტიზატორების შესწავლაზე გადავალთ, გავარკვიოთ თუ რა არის URI, URL და URN

URI

იშიფრება როგორც Uniform Resource Identifier, ანუ მუდმივი წყაროს იდენტიფიკატორი, წყაროში იგულისხმება ნებისმიერი რესურსი, რომელიც შეიძლება მოითხოვოს მომხმარებელმა: საიტის რომელიმე გვერდი, სურათი, css სტილი და ა.შ. კონკრეტული მაგალითი: https://vnadiradze.ge/info/laravel/index.html URI შედგება ორი ნაწილისაგან : URL და URN.

URL

იშიფრება როგორც Uniform Resource Locator ანუ მუდმივი წყაროს მაჩვენებელი, ლოკატორი. იგი მოიცავს წყაროსთან მიმართვის ტიპს (პროტოკოლს) და წყაროს მდებარეობას ანუ დომენს. კონკრეტული მაგალითი: http://vnadiradze.ge

URN

იშიფრება როგორც Uniform Resource Name ანუ მუდმივი წყაროს სახელი. იგი განსაზღვრავს წყაროს დასახელებას. კონკრეტული მაგალითი: /info/laravel/index.html

მარშრუტის შექმნა

HTTP GET

როგორც ვიცით მარშრუტები ინახება routes/web.php ფაილში. იმისათვის რათა შევქმნათ მარშრუტი პირველ რიგში უნდა მივმართოთ შესაბამის ფასადს - Route, თუ რა არის ფასადი, ამის შესახებ დაწვრილებით ოდნავ მოგვიანებით ვისაუბრებთ, ამ ეტაპზე კი შემოვიფარგლოთ შემდეგი განმარტებით : ფასადი არის კლასი, რომელიც ანხორციელებს წვდომას ფრეიმვორკ Laravel-ის ელემენტებთან. კონკრეტულ ფასადთან მიმართვისას ჩვენ რეალურად მივმართავთ config/app საქაღალდეში აღწერილი მასივის ბოლო ელემენტ aliases-სში მოთავსებული ფასადების ფსევდოკლასებიდან ერთ-ერთს. Route ფასადის შემთხვევაში ეს კლასი არის : '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');
});
            

HTTP POST

POST მეთოდით მოთხოვნის გასაგზავნად public საქაღალდეში დავამატოთ ფაილი form.html რომელშიც შევიტანთ რაიმე მარტივ ფორმას
<!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 მეთოდი, მეთოდს პირველ პარამეტრად გადეცემა მასივი, რომელშიც შეტანილია მოთხოვნათა სასურველი ტიპები :
Route::match(['get','post'] , '/comments' , function(){
    print_r($_POST);
});
             
ამ შემთხვევაში უკვე შეგვეძლება ფორმის გაგზავნის გარეშე http://127.0.0.1:8000/comments გვერდზე შესვლა.

მარშრუტის შექმნა ნებისმიერი ტიპის მოთხოვნისათვის

იმისათვის რათა მარშრუტი შეესაბამებოდეს ნებისმიერი ტიპის მოთხოვნას ერთდროულად, უნდა გამოვიყენოთ Route ფასადის any მეთოდი
Route::any('/comments' , function(){
    print_r($_POST);
});
            

მარშრუტისა და კონტროლერის დაკავშირება

მიუხედავად იმისა, რომ ჯერ არ ვიცნობთ კონტროლერებს, ოდნავ გავუსწოთ მოვლენებს და მოვიყვანოთ მაგალითი, თუ როგორ ხდება ამა თუ იმ მარშრუტთან კონკრეტული კონტროლერის დაკავშირება. პირველ რიგში routes/web.php ფაილში უნდა დავამყაროთ წვდომა შესაბამის კონტროლერთან, შემდეგ მივმართოთ სასურველ კონტროლერს და ამ კონტროლერის სასურველ მეთოდს :
use Illuminate\Support\Facades\Route;

use App\Http\Controllers\UserController;

Route::get('/user', [UserController::class, 'index']);
            

მარშრუტთა გადამისამართება

თუ გვჭირდება ისეთი მარშრუტის შექმნა, რომელიც სხვა URI-ზე უნდა გადამისამართდეს, მაშინ შეგვიძლია გამოვიყენოთ Route ფასადის redirect მეთოდი :
Route::redirect('/here', '/there');
            

წარმოდგენის მარშრუტები

თუ გვჭირდება ისეთი მარშრუტის შექმნა, რომელსაც უბრალოდ წარმოდგენის ფაილის დაბრუნება ევალება, მაშინ შეგვიძლია გამოვიყენოთ Route ფასადის view მეთოდი, რომელსაც პირველ პარამეტრად გადაეცემა URI ფრაგმენტი, მეორე პარამეტრად კი წარმოდგენის ფაილის დასახელება:
Route::view('/welcome', 'welcome');
            
ამის შემდეგ http://127.0.0.1:8000/welcome მისამართზე შესვლისას ჩაიტვირთება resources/views/welcome.blade.php ფაილი.

მარშრუტთა პარამეტრები

სავალდებულო პარამეტრები

აქამდე განხილულ მაგალითებში მარშრუტებს პირველ პარამეტრად გადავცემდით URI-ს კონკრეტულ ნაწილს, სტატიკურ სტრიქონს, მაგრამ შეიძლება დაგვჭირდეს ცვალებადი, დინამიური პარამეტრების გადაცემაც (მაგალითად კონკრეტული მომხმარებლის id-ის). ცვლადი პარამეტრის გადაცემის სინტაქსი ასეთია:
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

არასავალდებულო პარამეტრები

ამ მაგალითებში მარშრუტს ვუთითებდით ისეთ პარამეტრებს, რომელთა განსაზღვრაც აუცილებელი იყო, მაგრამ შეიძლება განისაზღვროს მარშრუტი ისეთი მოთხოვნისათვის რომელიც შეიძლება შეიცავდეს ან არ შეიცავდეს პარამეტრს, ასეთ შემთხვევაში პარამეტრის მითითებისას მას ეწერება კითხვის ნიშანი, ხოლო ფუნქცია დამმუშავებელს კი ეს პარამეტრი გადეცემა ნაგულისმები მნიშვნელობით - null
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 გვერდზეც.

მეთოდი where

შეიძლება მოხდეს ისე, რომ დაგვჭირდეს ცვლადი პარამეტრის ტიპის გაფილტვრა, მაგალითად id პარამეტრის მნიშვნელობა უნდა იყოს მხოლოდ და მხოლოდ რიცხვითი ტიპის, ამაში დაგვეხმარება რეგულარული გამოსახულებები და where მეთოდი, მისი გამოყენების სინტაქსი ასეთია:
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]+']);
            

მარშრუტის პარამეტრის ტიპის განსაზღვრა გლობალურად

შეიძლება მოხდეს ისე, რომ id პარამეტრის გაფილტვრა, ანუ ტიპის განსაზღვრა დაგვჭირდეს 100 მარშრუტისათვის, 100-ჯერ ერთი და იგივეს წერა, რა თქმა უნდა მოუხერხებელი და არაკომფორტულია. ასეთ შემთხვევაში დაგვეხმარება სერვის-პროვაიდერი ანუ კლასი app/Providers/RouteServiceProvider.php, კონკრეტულად კი მისი მეთოდი boot, ფრეიმვორკის ინსტალაციის პირველ ეტაპზე ეს მეთოდი გამოიყურება ასე:
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;
});
            

მარშრუტთა ჯგუფები

პროექტზე მუშაობისას შესაძლებელია მოხდეს ისე, რომ ბევრ URI-ს გააჩნდეს საერთო ფრაზა, საერთო სეგმენტი, მაგალითად : 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';
    });

});            
            

სახელდებული მარშრუტები, გადამისამართება გვერდებზე, შიდა ბმულები

ხშირადაა საჭირო პროექტის ერთი გვერდიდან მეორეზე გადასასვლელი ბმულების გაკეთება ან ავტომატურად გადასვლა (redirect). შიდა ბმულების შექმნისას საკმაოდ ხელსაყრელია მარშრუტებზე სახელის დარქმევა და შემდეგ ამ სახელების გამოყენება.
Route::get('/user/profile', function () {
    //
})->name('profile');
            
სახელის დარქმევა შესაძლებელია ასეც : Route::get('/user/profile', [UserProfileController::class, 'show'])->name('profile');
მარშრუტთა დასახელებები აუცილებლად უნდა იყოს უნიკალური.
სახელდებული მარშრუტის მიხედვით URL-ის გენერირება და გადამისამართება მოხდბა ასე:
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
            

მიმდინარე მარშრუტის გადამოწმება

თუ გვსურს დავადგინოთ მიმდინარე მარშრუტი მიემართა თუ არა კონკრეტული სახელდებული მარშრუტისაკენ, შეგვიძლია გამოვიყენოთ route დამხმარის named მეთოდი. მაგალითად გვაქვს მარშრუტთა შუამავალი და მასში გვინდა მარშრუტის სახელის გადამოწმება :
/**
 * 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 მეთოდი :
Route::middleware(['first', 'second'])->group(function () {

    Route::get('/', function () {
        // გაივლის first & second შუამავლებს...
    });

    Route::get('/user/profile', function () {
        // გაივლის first & second შუამავლებს...
    });

});
            

სადაზღვევო მარშრუტები :))

როდესაც შევდივართ ისეთ ბმულზე, რომლის შესაბამისი მარშრუტიც არ არის განსაზღვრული, აპლიკაციის გამონაკლისთა დამმუშავებელს ავტომატურად გადავყავართ 404 გვერდზე. შეგვიძლია შევქმნათ მარშრუტი, რომელში აღწერილი ფუნქციაც შესრულდება ყველა უმარშრუტო მისამართზე შესვლისას და აღარ მოხდება 404 გვერდზე გადამისამართება, ამაში დაგვეხმარება Route ფასადის fallback მეთოდი:
Route::fallback(function () {
    echo "გვერდი ვერ მოიძებნა";
});
            

მიმდინარე მარშრუტთან წვდომა

Route ფასადის current, currentRouteName და currentRouteAction მეთოდების დახმარებით შესაძლებელია მივიღოთ სხვადასხვა ინფორმაციები, მიმდინარე მოთხოვნის დამმუშავებელი მარშრუტის შესახებ :
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 საქაღალდეში.
გვახსოვდეს, რომ თუ გაკეთებული გვაქვს მარშრუტთა ქეშირება და ამ დროს შევქმნით სხვა ახალი მარშრუტს, მაშინ უნდა მოვახდინოთ მარშრუტთა ქეშის გასუფთავება და შემდეგ ხელახლა შევქმნათ იგი.
ქეშირების გასუფთავება შესაძლებელია Artisan-ის route:clear ბრძანებით : php artisan route:clear
8. შუამავლები




HTTP middleware ანუ შუამავალი კლასი არის HTTP მოთხოვნის დამამუშავებელი ფილტრი. მაგალითად Laravel-ში განხილულია შუამავალი კლასები, რომლებიც ამოწმებენ გავლილი აქვს თუ არა მომხმარებელს ავტორიზაცია. თუ კონკრეტულ მარშრუტთან დაკავშირებულია შესაბამისი შუამავალი კლასი და მომხმარებელს არ გაუვლია ავტორიზაცია ისე აკეთებს HTTP მოთხოვნას, მაშინ შუამავალი კლასი მას გადაამისამართებს ავტორიზაციის გვერდზე. თუ მარშრუტთან არ არის დაკავშირებული, მაშინ შუამავალი კლასი საქმეში არ ერთვება.

შუამავალი კლასის შექმნა

შუამავალი კლასის შექმნის სინტაქსი ასეთია : 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
];
            

გლობალური შუამავლები

თუ გვსურს, რომ შუამავალი გავრცელდეს ჩვენს აპლიკაციაში გაკეთებულ ყველა HTTP მოთხოვნაზე, მაშინ იგი უნდა აღვწეროთ app/Http/Kernel.php კლასის $middleware თვისებაში.

შუამავალი კლასის მიმაგრება მარშრუტებზე

ჩვენი შუალედური კლასის ფსევდონიმია checkIP, მივამაგროთ იგი მარშრუტს :
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 ფაილში კლასის სრული დასახელების გამოყენებითაცაა შესაძლებელი შუამავლების მიმაგრება მარშრუტებზე.
withoutMiddleware მეთოდი ვერ აუქმებს გლობალურ შუამავლებს, იგი ეხება მარტო app/Http/Kernel.php კლასის $routeMiddleware თვისებაში აღწერილ შუამავლებს.

შუამავალთა ჯგუფები

ზოგჯერ ხელსაყრელია, რომ რამდენიმე შუამავალი გაერთიანდეს ერთი გასაღების ქვეშ, ანუ მოხდეს მათი დაჯგუფება. გასაღების მეშვეობით შედარებით მარტივად მოხდება შუამავალთა გამოყენება სხვადასხვა ადგილას. ეს ჯგუფები აღწერილია app/Http/Kernel.php კლასის $middlewareGroups თვისებაში.

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'));
    });
}
            
9. კონტროლერები
ამ ეტაპზე ჩვენ უკვე ვიცით მარშრუტის შექმნა და მის დამმუშავებელ ფუნქციასთან მუშაობა, მაგრამ შევთანხდეთ, რომ თუ დამმუშავებლის კოდი საკმაოდ დიდია და მარშრუტებიც ბევრი გვექნება, ამ ყველაფრის ერთ ფაილში წერა მოუხერხებელი იქნება და საჭირო გახდება მარშრუტთა დამმუშავებლების ცალკე ფაილებში გატანა. ამაში დაგვეხმარება კონტროლერები.

კონტროლერები ინახება app/Http/Controllers საქაღალდეში. ნაგულისხმეობის პრინციპით ამ საქაღალდეში უკვე შექმნილია ერთი საბაზისო კონტროლერი Controller.php სწორედ მისი მემკვიდრეები უნდა იყვნენ ის კონტროლერები, რომლებსაც მომავალში შევქმნით.

კონტროლერის შექმნა

კონტროლერის დასახელება უნდა ემთხვეოდეს იმ კლასის დასახელებას, რომელსაც ამ კონტროლერში აღვწერთ. შევქმნათ კონტროლერი UserController.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 მისამართზე.

კონტროლერის შექმნა artisan ბრძანებით

კონტროლერების ხელით შექმნა ცოტა მოუხერხებელია, ამის გაკეთება გაცილებიდ მარტივია Laravel-ის კონსოლის - artisan-ის მეშვეობით. ბრძანებათა ველიდან გადავდივართ ჩვენი პროექტის საქაღალდეში, კონტროლერის შექმნის ბრძანების სინტაქსი კი ასეთია : 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');
    }
}
            

რესურსების კონტროლერები

წარმოვიდგინოთ, რომ აპლიკაციაში გვაქვს Photo და Movie მოდელები, რომელთა მეშვეობითაც შესაძლებელია რესურსების (ფოტო, ფილმი) შექმნა, წაკითხვა, რედაქტირება და წაშლა. ასეთ შემთხვევებში კონტროლერებს აქვთ ხოლმე, ოთხი ძირითადი მეთოდი : create, read, update, delete (CRUD). ნაცვლად იმისა, რომ სათითაო მეთოდისათვის, სათითაო ფუნქცია ვწეროთ ხელით კონტროლერში, შეგვიძლია კონტროლერის შექმნის ბრძანება გავუშვათ --resource ჩანაწერთან ერთად : 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

დამოკიდებულების ინექცია (Dependency Injection) და კონტროლერები

რა არის დამოკიდებულების ინექცია

ინგ: 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)
    {
        //
    }
}
            

მარშრუტთა დაჯგუფებები კონტროლერის მიხედვით

დავუშვათ გვაქვს პოსტებთან სამუშო კონტროლერი - PostsController, როგორც ვიცით, ლარაველის მე-8-ე ვერსიაში, შესაბამისი მარშრუტების დაგენერირება მოხდებოდა ასე :
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'); // ახალი პოსტის დამატება
});
            
როგორც ვხედავთ, მარშრუტებისათვის, სათითაოდ ცალ-ცალკე კონტროლერის განსაზღვრა აღარ გვჭირდება და უბრალოდ კონტროლერის მეთოდების დასახელებებს ვუთითებთ.
10. მოთხოვნის მიღება და დამუშავება, კლასი Request
აპლიკაციის გახსნის შემდეგ პირველი რაც ხდება ისაა, რომ სისტემა ღებულობს ამა თუ იმ შიგთავსის გამოტანის მოთხოვნას მომხმარებლისაგან. ამ თავში ვისაუბრებთ Illuminate\Http\Request კლასის შესახებ, რომლის მეშვეობითაც ხდება ნებისმიერი მოთხოვნის დამუშავება.

იმისათვის რათა მივიღოთ მიმდინარე 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) {
    //
});
            

Request კლასის მეთოდები

path() მეთოდი

ეს მეთოდი აბრუნებს URI-ს იმ ნაწილს, რომელიც შეიცავს მომხმარებლის მიერ გაკეთებული მოთხოვნის გზას, მაგალითად თუ URI არის http://127.0.0.1:8000/foo/bar, მეთოდი დააბრუნებს foo/bar - ს : $uri = $request->path(); // foo/bar

is() მეთოდი

ეს მეთოდი აბრუნებს მნიშვნელობას - true, თუ მომხმარებლის მიერ გაკეთებული მოთხოვნის შესაბამისი გზა (ანუ URI-ს ფრაგმენტი) ემთხვევა იმ შაბლონს, რომელსაც პარამეტრად გადავცემთ მეთოდს.
if ($request->is('foo/*')) 
{
    //
}
            

routeIs() მეთოდი

ეს მეთოდი აბრუნებს მნიშვნელობას - true, თუ მომხმარებლის მიერ გაკეთებული მოთხოვნა ემთხვევა, რომელიმე სახელდებულ მარშრუტს :
if ($request->routeIs('index')) 
{
    //
}
            

url(), fullUrl() და fullUrlWithQuery() მეთოდები

პირველი მათგარი აბრუნებს მიმდინარე მისამართს GET პარამეტრების გარეშე, ხოლო მეორე მათგანი - GET პარამეტრებთან ერთად :
// 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() და isMethod() მეთოდები

ეს მეთოდი აბრუნებს მომხმარებლის მიერ გაკეთებული მოთხოვნის ტიპს სტრიქონული სახით (get, post, ...). isMethod() მეთოდი აბრუნებს მნიშვნელობას - true, თუ გაკეებული მოთხოვნის ტიპი ემთხვევა არგუმენტად გადაცემული სტრიქონის მნიშვნელობას :
$method = $request->method();

if ($request->isMethod('get')) 
{
    // 
}
            

ip() მეთოდი

ეს მეთოდი აბრუნებს იმ მომხმარებლის IP მისამართს, რომელმაც მოთხოვნა გააკეთა ჩვენს აპლიკაციაში : $ipAddress = $request->ip();

all() მეთოდი

ეს მეთოდი აბრუნებს ასოციაციურ მასივს, რომლის ინდექსებიცაა html ფორმის ველთა დასახელებები, ხოლო ამ ინდექსების მნიშვნელობები კი ველებში შეყვანილი ინფორმაციებია, მაგალითად თუ გავგზავნით (submit) ასეთი ველების მქონე ფორმას : <input type="text" name="name">
<input type="password" name="password">
public function login(Request $request)
{
    print_r($request->all());
}
            
შედეგად ვიხილავთ დაახლოებით შემდეგი სახის მასივს :
Array
(
    [name] => vaso
    [password] => pass123
)
            

input() მეთოდი

ეს მეთოდი გამოიყენება მოთხოვნიდან კონკრეტული ველის მნიშვნელობის ამოსაღებად, პირველ პარამეტრად მეთოდს უნდა გადავცეთ ველის დასახელება, მეორე პარამეტრად კი შეგვიძლია გადავცეთ ალტერნატიული მნიშვნელობა ისეთი ველისათვის, რომელიც მოთხოვნაში არ იქნება:
// 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 მოთხოვნებთან.

query() მეთოდი

ეს მეთოდი მუშაობს მხოლოდ GET ტიპის მოთხოვნებთან, შეგვიძლია მასაც გადაცხეთ ალტერნატიული მნიშვნელობა არარსებული ველისათვის :
$name = $request->query('name', 'Helen');
            
თუ მეთოდს საერთოდ არ გადავცემთ პარამეტრებს, მაშინ იგი დაგვიბრუნებს GET ტიპის ყველა პარამეტრს.

boolean() მეთოდი

როდესაც ვამუშავებთ ისეთ HTML ელემენტებს, როგორიცაა მაგალითად 'checkbox', შესაძლებელია, რომ აპლიკაციამ მათი მონიშნულობის (მოპწიჩკულობის :) ) აღმნიშვნელად მიიღოს სტრიქონული ტიპის ტექსტები, მაგალითად : 'true' ან 'on'. სწორედ ასეთ ელემენტებთან სამუშაოდ გამოიყენება boolean მეთოდი. იგი აბრუნებს ჭეშმარიტ მნიშვნელობას (true), 'checkbox' ტიპის ველის 'name' ატრიბუტის შემდეგი მნიშვნელობებისათვის : 1, '1', true, 'true', 'on', და 'yes', ყველა სხვა შემთხვევაში ბრუნდება მნიშვნელობა false. $archived = $request->boolean('archived');

ველის ამოღება დინამიური მეთოდების დახმარებით

შესაძლებელია, რომ მოთხოვნის ველები განვიხილოთ როგორც Illuminate\Http\Request კლასის ობიექტის თვისებები და ისე მივწვდეთ მათ. მაგალითად თუ აპლიკაციაში გვაქვს ფორმა, რომლის ერთ-ერთი ველის დასახელებაცაა 'name', ამ ველის მნიშვნელობა შეიძლება გავიოთ ასე : $name = $request->name; დინამიური მეთოდის გამოყენებისას ფრეიმვორკი პირველ რიგში გადაამოწმებს მოთხოვნაში ჩადებულ ველებს, თუ აქ ვერ მოიძებნა შესაბამისი ველი, მაშინ გადაამოწმებს მარშრუტის პარამეტრებს.

only() მეთოდი

ეს მეთოდი მუშაობს all მეთოდის ანალოგიურად, იმ განსხვავებით, რომ თუ ეს უკანასკნელი აბრუნებს გაგზავნილი ინფორმაციის შემცველი მასივის ყველა ინდექსსა და მათ მნიშვნელობებს, only მეთოდი აბრუნებს მხოლოდ იმ ინდექსებს, რომლებსაც პარამეტრებად გადავცემთ :
$input = $request->only(['username', 'password']);

$input = $request->only('username', 'password');
            
მეთოდს პარამეტრად უნდა გადაეცეს შესაბამისი ველის დასახელება.

except() მეთოდი

ეს მეთოდი არის all და only მეთოდების ერთფგვარი შებრუნებული მეთოდი :)) იგი აბრუნებს გაგზავნილი ინფორმაციის შემცველ მასივს იმ ინდექსების გამოკლებით, რომლებსაც პარამეტრებად გადავცემთ :
$input = $request->except(['credit_card']);

$input = $request->except('credit_card');
            
მეთოდს პარამეტრად უნდა გადაეცეს შესაბამისი ველის დასახელება (name ატრიბუტის მნიშვნელობა)

has() მეთოდი

მეთოდი აბრუნებს მნიშვნელობას - true თუ მოთხოვნაში ჩადებული ინფორმაციის შემცველი მასივი შეიცავს იმ ინდექსს, რომელსაც პარამეტრად გადავცემთ მეთოდს, წინააღმდეგ შემთხვევაში მეთოდი აბრუნებს მნიშვნელობას false.
if ($request->has('name')) 
{
    //
}
            

filled() მეთოდი

მეთოდი აბრუნებს მნიშვნელობას - true თუ მოთხოვნაში ჩადებული ინფორმაციის შემცველი მასივი შეიცავს იმ ინდექსს, რომელსაც პარამეტრად გადავცემთ მეთოდს და ამასთანავე ველის მნიშვნელობა არ იქნება ცარიელი, წინააღმდეგ შემთხვევაში მეთოდი აბრუნებს მნიშვნელობას false.
if ($request->filled('name')) 
{
    //
}
            

flash() მეთოდი

შესაძლებელია საჭირო გახდეს მომხმარებლის მიერ აკრეფილი ინფორმაციის გადამოწმება, ვალიდაცია. თუ ეს ინფორმაცია არ აკმაყოფილებს ვალიდაციის პირობებს და თუ ფორმაც მარტივია და შედგება რამდენიმე ველისაგან, მომხმარებელს უბრალოდ გადავამისამართებთ ისევ ფორმის გვერდზე, მაგრამ თუ ფორმა რთულია და შეიცავს ძალიან ბევრ ველებს, მაშინ მომხმარებელს ამ ველების თავიდან შევსება მოუწევს, რაც არც თუ ისე მოსახერხებელია. ამ პრობლემის გადასაწყვეტად გამოიყენება request ობიექტის flash() მეთოდი, რომელიც მოთხოვნის ტანში ჩადებულ ინფორმაციას ინახავს სესიაში. $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') }}">
            
ამის შემდეგ თუ ფორმას გავაგზავნით ვნახავთ, რომ აკრეფილი ინფორმაცია არ დაიკარგება და ველები ავტომატურად შეივსება.

flashOnly() მეთოდი

ეს მეთოდი სესიაში ინახავს მხოლოდ იმ ველების მნიშვნელობებს, რომლებსაც გადავცემთ პარამეტრებად : $request->flashOnly(['username', 'email']);

flashExcept() მეთოდი

ეს მეთოდი სესიაში ინახავს ყველა ველის მნიშვნელობას გარდა იმ ველებისა, რომლებსაც გადავცემთ პარამეტრებად : $request->flashExcept('password');
11. პასუხი სერვერიდან, კლასი Response
წინა თავში განვიხილეთ თუ როგორ ხდება მოთხოვნის მიღება და დამუშავება. მოთხოვნის გაშვების შემდეგ, ბუნებრივია სერვერიდან ბრუნდება პასუხი. ამ თავში განვიხილავთ თუ როგორ აკეთებს Laravel-ი ამას და რა საშუალებები არსებობს პასუხის რეალიზებისათვის.

კონკრეტულ გვერდზე რაიმე ინფორმაციის გამოტანის, ანუ ერთგვარი პასუხის დაბრუნების უმარტივესი გზა არის კონტროლერში ან მარშრუტის დამმუშავებელში ტექსტის დაბრუნება :

public function index()
{
    return 'Hello World';
}
            
Route::get('/', function () {
    return 'Hello World';
});
            
ასევე შესაძლებელია მასივის დაბრუნებაც, ფრეიმვორკი ავტომატურად გადააფორმატებს მას JSON ფორმატში :
Route::get('/', function () {
    return [1, 2, 3];
});
            

პასუხის ობიექტი

მოთხოვნათა პასუხების აბსტრაქცია ანუ განზოგადებული სახე არის Response კლასი, რომელიც აღწერილია vendor/laravel/framework/src/Illuminate/http/Response.php ფაილში. ამ კლასის მეთოდ-კონსტრუქტორს თუ დავაკვირდებით, გავიგებთ რა პარამეტრები შეგვიძლია გადავცეთ პასუხებთან სამუშაო დამხმარე ფუნქცია response-ს :
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');
});
            

Cookie-ს მიმაგრება პასუხზე

პასუხზე Cookie-ს მიმაგრება შესაძლებელია cookie მეთოდით, მეთოდს უნდა გადავეთ სამი პარამეტრი : დასახელება, მნიშვნელობა და Cookie-ს ქმედითუნარიანობის ხანგრძლივობა წუთებში : return response('Hello World')->cookie('name', 'value', $minutes);

Cookie-ს წაშლა პასუხიდან

პასუხიდან Cookie-ს წასაშლელად გამოიყენება withoutCookie მეთოდი, რომელსაც პარამეტრად უნდა გადავცეთ Cookie-ს დასახელება : return response('Hello World')->withoutCookie('name');

Cookie-ბი და შიფრაცია

ნაგულისხმეობის პრინციპით Laravel-ი ახდენს Cookie-ბის შიფრაციას, შესაბამისად შენახული ინფორმაცია მომხმარებლისათვის არ არის ხელმისაწვდომი. თუ გვსურს, რომ არ მოხდეს რომელიღაც კონკრეტული Cookie-ს შიფრაცია, მაშინ უნდა გამოვიყენოთ App\Http\Middleware\EncryptCookies შუამავლის $except მეთოდი :
/**
 * იმ cookie-ბის დასახელებები, რომელთა შიფრაციაც არ მოხდება
 *
 * @var array
 */
protected $except = [
    'cookie_name',
];
            

გადამისამართებები

რომელიმე გვერდზე გადასამისამართებლად გამოიყენება Illuminate\Http\RedirectResponse კლასის ობიექტები,არსებობს ამ ობიექტებთან წვდომის რამდენიმე ვარიანტი, ერთ-ერთია დამხმარე ფუნქცუა redirect, რომელსაც პარამეტრად უნდა გადავცეთ ბმული სადაც გვსურს გადამისამართება :
Route::get('/dashboard', function () {
    return redirect('home/dashboard');
});
            
ზოგჯერ საჭიროა, რომ მომხმარებელი გადავამისამართოთ წინა გვერდზე (მაგალითად გაგზავნა ფორმა არავალიდური ინფორმაციებით), ანუ დავაბრუნოთ უკან, ამისათვის გამოიყენება დამხმარე ფუნქცია back. ფუნქცია იყენებს სესიებს, ამიტომ დარწმუნებულები უნდა ვიყოთ, რომ მარშრუტი, რომლის დამმუშავებელშიც back ფუნქციას ვიძახებთ, მოქცეულია web შუამავალში (როგორც ვიცით სწორედ ეს შუამავალი ახდენს, სესიების მიმაგრებას მარშრუტებთან) :
Route::post('/user/profile', function () {
    
    // მოთხოვნის ვალიდაცია...

    return back()->withInput();

});
            

გადამისამართება სახელდებულ მარშრუტებზე

სახელდებულ მარშრუტზე გადასამისამართებლად, გამოიყენება redirect ფუნქციის route მეთოდი, რომელსაც პარამეტრად უნდა გადავცეთ მარშრუტის დასახელება : return redirect()->route('login'); თუ მარშრუტს აქვს პარამეტრები, მათი გადაცემა შეგვიძლია მეთოდის მეორე არგუმენტად :
// მარშრუტი შემდეგი URI-სათვის : /profile/{id}

return redirect()->route('profile', ['id' => 1]);
            

გადამისამართება გარე ბმულებზე

ხანდახან საჭიროა, რომ გადამისამართება მოვახდინოთ ისეთ ბმულზე, რომელიც მდებარეობს ჩვენი აპლიკაციის გარეთ. ასეთ შემთხვევაში უნდა გამოვიყენოთ redirect ფუნქციის away მეთოდი, რომელსაც პარამეტრად გადაეცემა სასურველი ბმული : return redirect()->away('https://www.google.com');

გადამისამართება სესიის ინფორმაციასთან ერთად

გადამისამართებისას შეიძლება დაგვჭირდეს სესიაში რაიმე ინფორმაციის შენახვა და შემდეგ ამ ინფორმაციის იმ გვერდზე გამოყენება, სადაც გადამისამართებას ვაკეთებთ. ამ შემთხვევაში გამოიყენება redirect ფუნქციის with მეთოდი, რომელსაც პარამეტრად გადაეცემა სესიის გასაღები და ამ გასაღების შესაბამისი მნიშვნელობა :
Route::post('/user/profile', function () {
    // ...

    return redirect('dashboard')->with('status', 'ინფორმაცია წარმატებით განახლდა !');
});
            
გადამისამართების შემდეგ, წარმოდგენის ფაილში ამ ინფორმაციის გამოყენების სინტაქსი იქნება ასეთი :
@if (session('status'))
    <div class="alert alert-success">
        {{ session('status') }}
    </div>
@endif
            

JSON პასუხები

response ფუნქციის json მეთოდი, პასუხის სათაურ (header) Content-Type-ის მნიშვნელობად ავტომატურად სვამს application/json-ს.
return response()->json([
    'name' => 'Abigail',
    'state' => 'CA',
]);
            

ფაილის გადმოწერა

თუ გვსურს, რომ დაბრუნებული პასუხის შედეგად მომხმარებლის ბრაუზერმა მოახდინოს კონკრეტული ფაილის გადმოწერა, უნდა გამოვიყენოთ response ფუნქციის download მეთოდი, რომელსაც პარამეტრად უნდა გადავცეთ სასურველი ფაილის მისამართი : return response()->download($pathToFile);
12. წარმოდგენის შაბლონები, ინფორმაციის მიმაგრება მათზე
რა თქმა უნდა საკმაოდ არაკომფორტულია HTML კოდების პირდაპირ მარშრუტებში და კონტროლერებში დაბრუნება. წარმოდგენა არის MVC შაბლონის ერთ-ერთი ელემენტი, რომელიც უზრუნველჰყოფს ინფორმაციის ბრაუზერში გამოტანას და მისი საშუალებით ასევე ხდება HTML შიგთავსების, ძირითადი ლოგიკისაგან გამოყოფა, განცალკევება. წარმოდგენის ფაილები ინახება resources/views საქაღალდეში. მოვიყვანოთ წარმოდგენის უმარტივესი მაგალითი :
<!-- 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' => 'შამილი']);
წარმოდგენის ფაილის დასახელება არ უნდა შეიცავდეს . სიმბოლოს (წერტილს)
ამ თავში ვისაუბრებთ წარმოდგენებზე, რომლებიც იმუშავებენ შაბლონიზატორ blade-ს გარეშე.

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);

with მეთოდი

ინფორმაციის მიმაგრება შესაძლებელია with მეთოდითაც : 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 მეთოდის დასახელება პრეფიქსად ერთვის ცვლადის დიდი ასოთი დაწყებულ სახელს, შემდეგ კი ფრხილებში ეთითება ცვლადის მნიშვნელობა.

compact მეთოდი

წარმოდგენის ფაილზე ინფორმაციის მიმაგრების კიდევ ერთი, არანაკლებ ეფექტური მეთოდია compact. ეს არის PHP-ს სტანდარტული ფუნქცია, რომელიც ცვლადთა დასახელებებისა და მათი მნიშვნელობებისაგან აკეთებს ასოციაციურ მასივს :
$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>
            

view()->exists()

როგორ გადავამოწმოთ კონტროლერში, არსებობს თუ არა წარმოდგენის ესა თუ ის ფაილი ? როგორც ვიცით view() ფუნქცია ქმნის გლობალური view კლასის ობიექტს, ამ ობიექტს გააჩნია მეთოდი exists(), რომელიც ამოწმებს არსებობს თუ არა წარმოდგენის კონკრეტული ფაილი. მეთოდს პარამეტრად გადეცემა წარმოდგენის ფაილის დასახელება და აბრუნებს true მნიშვნელობას თუ ეს ფაილი არსებობს. კონტროლერში შეგვიძლია ჩავწეროთ ასეთი რამ :
public function index()
{
    if (view()->exists('templates.template'))
    {
        return view('templates.template')->withTitle('Hello World');
    }
}            
            

ინფორმაციის გაზიარება წარმოდგენის ყველა ფაილისათვის

ინფორმაციის გაზიარება წარმოდგენის ყველა ფაილისათვის შესაძლებელია View ფასადის share მეთოდით. როგორც წესი, ეს გაზიარება აღიწერება ხოლმე სერვისპროვაიდერის boot მეთოდში :
namespace App\Providers;

use Illuminate\Support\Facades\View;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        //
    }
    
    public function boot()
    {
        View::share('key', 'value');
    }
}         
            

წარმოდგენის ფაილების ქეშირება

ნაგულისხმეობის პრინციპით ფრეიმვორკი ახდენს წარმოდგენათა ფაილების კომპილაციას. როდესაც სრულდება მოთხოვნა, რომლის შედეგადაც უნდა მოხდეს წარმოდგენის ჩატვირთვა, Laravel-ი ადგენს არსებობს თუ არა საჭირო წარმოდგენის კომპილირებული ვერსია, თუ ეს ვერსია არსებობს ამის შემდეგ სისტემა დაადგენს რომელი ვერსია უფრო გვიან შეიცვალა - კომპილირებული თუ არაკომპილირებული. თუ კომპილირებული ვერსია საერთოდ არ არსებობს ან არსებობს, მაგრამ არაკომპილირებულში მოხდა ცვლილებები მაშინ გაკეთდება წარმოდგენის ფაილის ხელახალი კომპილაცია.

შესაძლებელია, რომ ამ პროცესმა უარყოფითი გავლენა იქონიოს აპლიკაციის სისწრაფეზე, ასე რომ view:cache Arttisan ბრძანებით ეგვიძლია დავქეშოთ წარმოდგენის ფაილები :

php artisan view:cache ქეშირებული შაბლონები შეინახება storage/framework/views საქაღალდეში. ქეშის გასუფთავება კი მოხდება ასე : php artisan view:clear
13. შაბლონიზატორი Blade
ამ თავში ვისაუბრებთ შაბლონიზატორ blade-ზე, რომელიც ჩაშენებულია Laravel-ის ფუნქციონალში. შაბლონიზატორი არის სპეციალური მექანიზმი, რომელიც გამოიყენება შაბლონის კონკრეტულ ადგილებზე ინფორმაციის მისამაგრებლად და გვერდის საბოლოო იერსახის ჩამოსაყალიბებლად. თავის მხრივ წარმოდგენა არის PHP ფაილი, რომელშიც აღწერილია HTML კოდი და აგრეთვე სპეციალური ნიშნულები რომელთა ნაცვლადაც უნდა ჩაჯდეს პროექტის ლოგიკური ნაწილიდან მოსული ინფორმაციები. blade შაბლონიზატორში არ უნდა ეწეროს PHP კოდი. იმდენად რამდენადაც გვერდის გაფორმებისათვის აუცილებელი ყველანაირი ლოგიკა ჩანაცვლებულია სპეციალური ნიშნულებითა და შაბლონიზატორის კონსტრუქციებით, რომელთა გადათარგმნა და ლოგიკის კოდით ჩანაცვლება ხდება კომპილაციის დროს.

კომპილირებული შაბლონები ინახება storage/framework/views საქაღალდეში.

იმისათვის რათა წარმოდგენის ფაილი blade შაბლონიზატორმა დაამუშავოს, ფაილის გაფართოება უნდა იყოს blade.php. გადავარქვათ ჩვენს მიერ შექმნილ ფაილს სახელი:

template.blade.php ამ ფაილს ახლა უკვე ამუშავებს შაბლონიზატორი Blade.

ინფორმაციის გამოტანა

იმისაათვის რათა შაბლონში გამოვიტანოთ მასზე მიმაგრებული ინფორმაცია, უნდა გამოვიყენოთ ორმაგი ფიგურული ბრჭყალები :
Route::get('/', function () {
    return view('templates.template', ['name' => 'შამილი']);
});
            
წარმოდგენის ფაილი : {{ $name }} გამარჯობა ! არ არის აუცილებელი მაინცდამაინც ცვლადის სახით მიმაგრებული ინფორმაცია გამოვიტანოთ ამ გზით, შესაძლებელია PHP-ს ნებისმიერი ფუნქციის შედეგის გამოტანაც : მიმდინარე UNIX დროითი ნიშნული არის {{ time() }}

JSON ფორმატის ინფორმაცია

შეიძლება მოხდეს ისე, რომ დაგვჭირდეს PHP მასივის JSON ფორმტში გადაყვანა და შედეგის JavaScript ცვლადში მოქცევა :
<script>
    var app = <?php echo json_encode($array); ?>;
</script>
            
იგივეს გაკეთება უფრო მარტივადაა შესაძლებელი შაბლონიზატორის @json დირექტივის გამოყენებით :
<script>
    var app = @json($array);
</script>
            

HTML გარემოს გამოტანა

შევქნათ კონტროლერი და აღვწეროთ ამდაგვარი მეთოდი :
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 ოპერატორთან მუშაობა ხდება @if, @elseif, @else და @endif დირექტივების მეშვეობით :
@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 და @guest დირექტივების მეშვეობით უმარტივესად ხდება იმის დადგენა აუტენტიფიცირებულია მომხმარებელი თუ არა :
@auth
    // აუტენტიფიცირებულია
@endauth

@guest
    // არააუტენტიფიცირებულია
@endguest
            

switch ინსტრუქცია

switch ინსტრუქციებთან მუშაობა ხდება @switch, @case, @break, @default და @endswitch დირექტივების მეშვეობით :
@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
            

ციკლის ცვლადი $loop

ციკლებთან მუშაობისას საშუალება გვაქვს მივწვდეთ $loop ცვლადს, რომელიც გვიმარტივებს ამ პროცესს.
@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 კომენტარებისაგან განსხვავებით ბლეიდის კომენტარები არ შედის გენერირებულ HTML კოდში: {{-- ეს კომენტარი არ შევა დაგენერირებულ HTML-ში --}}

წარმოდგენის ფაილის ჩასმა წარმოდგენის ფაილში

შაბლონიზატორის @include დირექტივის მეშვეობით საშუალება გვაქვს წარმოდგენის ფაილში ჩავსვათ სხვა წარმოდგენის ფაილები. მშობელ ფაილზე მიმაგრებული ნებისმიერი ცვლადი ხელმისაწვდომია შვილობილ ფაილშიც :
<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 ბლეიდში

ზოგჯერ საჭიროა, რომ წარმოდგენის ფაილში სტანდატული PHP კოდი შევიტანოთ, ამისათვის გამოიყენება @php დირექტივა :
@php
    $counter = 1;
@endphp
            

მშობელი შაბლონები, წარმოდგენის ფაილების სტუქტურის გამართვა

ხშირად ხდება ისე, რომ საიტის სხვადასხვა გვერდებზე გვხვდება ერთი და იგივე ფრაგმენტები, მაგალითად საიტის ქუდი (header), საიტის ძირი (footer), ნავიგაციური მენიუ და ა.შ. ბუნებრიბვია გაუმართლებელია ამ ფრაგმენტების ყველა გვერდისათვის სათითაოდ გაკეთება და ჯობია თუ მათ ცალკე ფაილებში გავიტანთ, შემდეგ კი სხვადასხვა ადგილებში გამოვიყენებთ საჭიროებისამებრ.

განვიხილოთ მშობელი შაბლონის მარტივი მაგალითი :

<!-- 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
            

ფორმები

CSRF

CSRF იშიფრება, როგორც Cross-site request forgeries - საიტთა შორის ყალბი მოთხოვნების გაცვლა-გამოცვლის პროცესი, რომლის დროსაც ხდება არაავტორიზირებული მოთხოვნების შესრულება აუტენტიფიცირებული მომხმარებლის სახელით.

განვიხილოთ მოქმედებათა ასეთი ჯაჭვი:

  • წარმოვიდგინოთ, რომ რომელიმე ონლაინ-ბანკში (პირობითად www.mybank.com) შესულები ვართ პირად კაბინეტში.
  • ასევე დავუშვათ, რომ თანხის ტრანსფერის განხორციელება შესაძლებელია http://www.mybank.com/transfer?to=<SomeAccountnumber>;amount=<SomeAmount> ბმულზე მიკითხვით (ჩვენი ანგარიშის ნომერი ან რაიმე სხვა იდენტიფიკატორი აღარაა საჭირო რადგან, აუტენტიფიკაცია გავლილი გვაქვს და სისტემა უკვე ისედაც 'გვცნობს').
  • დავუშვათ ახალ ფანჯარაში გავხსენით www.cute-cat-pictures.org ჰაკერული საიტი, მაგრამ არ ვიცით რომ აქ შესვლა სახიფათოა :))
  • თუ ამ საიტის მფლობელმა იცის რომელ ბმულს უნდა მიაკითხოს ტრანზაქციის განსახორციელებლად (ამის გაგება საკმაოდ მარტივია, ბოლოს და ბოლოს თვითონაც იქნება დარეგისტრირებული www.mybank.com საიტზე :))) და ასევე იცის, რომ დროის კონკრეტულ მომენტში თქვენ სისტემაში ხართ შესული, მას შეუძლია თავის საიტზე განათავსოს ასეთი ბმული : http://www.mybank.com/transfer?to=123456;amount=10000 (სადაც 123456 არის მისი ანგარიშის ნომერი, 10000 კი არის თანხა, რომლის გადარიცხვასაც ცდილობს თქვენი ანგარიშიდან.
  • www.cute-cat-pictures.org შესვლისას ჩვენი ბრაუზერი ავტომატურად გააკეთებს ამ მოთხოვნას.
  • საბანკო სისტემა ვერ დაადგენს თუ საიდან წამოვიდა მოთხოვნა, რადგან როგორც ზემოთ ვთქვით სისტემაში შესულები ვართ ანუ ბრაუზერში შენახულია საჭირო Cookie-ბი, სისტემა ჩათვლის, რომ მოთხოვნა ლეგალურია და ჩამოგვეჭრება 10000 ლარი ))

ახლა განვიხილოთ ასეთი შემთხვევა:

  • ტრანსფერის მოთხოვნას დავამატოთ კიდევ ერთი პარამეტრი : http://www.mybank.com/transfer?to=123456;amount=10000;token=31415926535897932384626433832795028841971.
  • მესამე პარამეტი token არის რთულად გამოსაცნობი შემთხვევით სტრიქონი, რომლის გენერირებასაც mybank.com საიტი მოახდენს მისი გვერდების თითოეული ჩატვირთვისას. ეს სტრიქონი განსხვავებული იქნება ნებისმიერი გვერდის ყოველ ახალ ჩატვირთვაზე.
  • თავდამსხმელს არ მიუწვდება ხელი ამ თოქენთან და შესაბამისად არც ის შეუძლია რომ იგი მოთხოვნას გამოაყოლოს, არასწორი თოქენის შემცველი ან საერთოდდ უთოქენოდ გამოგზავნილი მოთხოვნები კი უარყოფილი იქნება www.mybank.com-ის მიერ.

CSRF ველი

ყოველთვის როცა ჩვენს ნებისმიერ აპლიკაციაში აღვწერთ HTML ფორმას, აუცილებლად უნდა განვსაზღვროთ ერთი დამალული ველი (type="hidden") CSRF თოქენისათვის, ფორმით გაგზავნილი მოთხოვნა კი უნდა დაამუშავოს CSRF თავდასხმისაგან დამცავმა შესაბამისმა შუამავალმა შუამავალმა. ამ ველის გენერირება შესაძლებელია @csrf დირექტივის მეშვეობით :
<form method="POST" action="/profile">
    @csrf

    ...
</form>
            

შიგთავსის სახელდებული ფრაგმენტები, დასტები

შაბლონიზატორი ბლეიდი საშუალებას გვაძლევს წარმოდგენის ფაილში განვათავსოთ, სადღაც სხვა წარმოდგენის ფაილში აღწერილი შიგთავსის (მარქაფის) სახელდებული ფრაგმენტები. მაგალითად თუ გვაქვს რამდენიმე შვილობილი შაბლონი, რომლებსაც თავიანთი კონკრეტული Javascript ფაილები ესაჭიროებათ, შეგვიძლია მოვიქცეთ ასე, მშობელი შაბლონი :
<!-- 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
            
14. მუშაობა URL-ებთან
Laravel-ში შექმნილია რამდენიმე დამხმარე ფუნქცია, რომლებიც გვიმარტივებენ URL-ებთან მუშაობას. ამ ფუნქციებთან მუშაობა განსაკუთრებით კომფორტულია, როდესაც გვჭირდება სხვადასხვა ბმულების გენერირება ჩვენს აპლიკაციაში ან კონკრეტული მოთხოვნის ისეთი პასუხს ვაგენერირებთ, რომლის მიხედვითაც აპლიკაციის ერთი ნაწილიდან მეორეში გვიწევს გადამისამართება.

ბმულების გენერირება

ბმულების გენერირებისათვის გამოიყენება url დამხმარე :
$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();               
            

URL-ები სახელდებული მარშრუტებისათვის

სახელდებული მარშრუტებისათვის ბმულების გენერირებაში დაგვეხმარება route ფუნქცია. ასეთი ბმულების შექმნისას, თუ უშუალოდ მარშრუტში აღწერილი ბმული შეიცვლება ჩვენ არ მოგვიწევს არანაირი ცვლილება route ფუნქციის გამოძახებისას, სისტემა ავტომატურად დააგენერირებს მარშრუტის ახალი ინსტრუქციის შესაბამის ბმულს. მაგალითად, დავუშვათ გვაქვს ასეთი მარშრუტი :
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           
            

URL-ები კონტროლერების მეთოდებისათვის

action მეთოდის მეშვეობით შეგსაძლებელია, რომ დავაგენერიროთ ბმული კონკრეტული მარშრუტის კონკრეტული მეთოდისათვის :
use App\Http\Controllers\HomeController;

$url = action([HomeController::class, 'index']);  
            
თუ კონტროლერის მეთოდს გადაეცემა მარშრუტის პარამეტრები, შეგვიძლია ისინი აღვწეროთ ასოციაციურ მასივში და ეს მასივი action ფუნქციას გადავცეთ მეორე პარამეტრად : $url = action([UserController::class, 'profile'], ['id' => 1]);
15. სესიები
სესიათა მექანიზმის პარამეტრები აღწრილია config/session.php ფაილში სადაც ბრუნდება მასივი. განვიხილოთ ძირითადი პარამეტრები : '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-ს. ანუ სესიის ინფორმაციები შეინახება მბ-ში და კერძოდ აქ მითითებულ ცხრილში.

დრაივერი database

როგორც აღვნიშნეთ, 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
...
             

მუშაობა სესიებთან

სესიებთან მუშაობა $request ობიექტით

get()

მოთხოვნის ობიექტს - $request-ს აქვს დამხმარე მეთოდი session, რომელიც გვაძლევს სესიებთან წვდომის საშუალებას. კონკრეტული სესიის მნიშვნელობის მისაღებად session დამხმარე უნდა გამოვიყენოთ get მეთოდთან ერთად, სესიებში ინფორმაციები ინახება წყვილების სახით "გასაღები:მნიშვნელობა", get მეთოდს პირველ პარამეტრად უნდა გადავცეთ სესიის სასურველი გასაღების დასახელება, მეორე პარამეტრად კი შეგვიძლია გადავცეთ ნაგულსხმები მნიშვნელობა იმ შემთხვევისათვის თუ სესიაში ეს გასაღები ვერ მოიძებნება :
public function show(Request $request)
{
    $result = $request->session()->get('key','არ არსებობს');
    dump($result);
}
          
სესიაში უჯრა სახელად key არ არსებობს, ამიტომ ბრაუზერში დაგვიბრუნდება "არ არსებობს".

all()

თუ გვინდა, რომ დავაბრუნოთ სესიაში შენახული ყველა ინფორმაცია უნდა გამოვიყენოთ session დამხმარეს all მეთოდი, რომელიც სესიას აბრუნებს მასივის სახით :
public function show(Request $request)
{
    $result = $request->session()->all();
    dump($result);
}
            

put()

სესიაში ინფორმაციის შეტანა ხდება put მეთოდის მეშვეობით, მას პარამეტრებად უნდა გადაეცეს სესიის უჯრის დასახელება და შესაბამისი მნიშვნელობა :
public function show(Request $request)
{
    $request->session()->put('key','value');
    $result = $request->session()->all();
    dump($result);
}
             
ამ კოდის შედეგი იქნება დაახლოებით ამდაგვარი რამ :

has()

იმის გასაგებად არსებობს თუ არა კონკრეტული დასახელების სესია, გამოიყენება has მეთოდი, რომელსაც პარამეტრად უნდა გადაეცეს საძიებელი სესიის დასახელება :
public function show(Request $request)
{
    if ($request->session()->has('key'))
    {
        dump("1");
    }
    else
    {
        dump("0");
    }
}
            

flash()

ხანდახან საჭოროა, რომ სესიაში ინფორმაცია შევინახოთ მხოლოდ შემდეგი მოთხოვნის დამუშავებამდე, შემდეგ კი წავშალოთ ეს ინფორმაცია სესიიდან. ამისათვის უნდა გამოვიყენოთ flash მეთოდი, რომლის გამოყენებაც საკმაოდ ეფექტურ შედეგს იძლევა დროებითი თვალსაჩინოებისათვის გამოყენებული შეტყობინებების გამოტანისას : $request->session()->flash('status', 'შეტყობინება წარმატებით გაიგზავნა !'); ამ ინფორმაციის ნახვა კი ასე შეგვიძლია წარმოდგენის ფაილში :
@if(Session::has('status'))
    <p>{{ Session::get('status') }}<p>
@endif
            

სესიის მნიშვნელობის გაზრდა და შემცირება

თუ სესიაში შენახული გვაქვს მთელი ტიპის მნიშვნელობა და გვსურს მისი გაზრდა ან შემცირება, უნდა გამოვიყენიოთ increment და decrement მეთოდები :
$request->session()->increment('count');

$request->session()->increment('count', $incrementBy = 2);

$request->session()->decrement('count');

$request->session()->decrement('count', $decrementBy = 2);              
            

Session ფასადი

ზემოთ აღწერილი მეთოდები შეგვიძლია გამოვიყენოთ Session ფასადთანაც. მაგალითად დავბეჭდოთ სესია წარმოდგენის ფაილში : {{ dump(Session::all()) }} ამ ფასადის გამოყენება, რა თქმა უნდა შესაძლებელია კონტროლერშიც.

სესიის კონკრეტული უჯრის წასაშლელად გამოიყენება forget მეთოდი, რომელსაც პარამეტრად უნდა გადავცეთ შესაბამისისი უჯრის დასახელება :

Session::forget('key2'); სესიის მთლიანად წასაშლელად გამოიყენება flush მეთოდი, რომელსაც პარამეტრის გადაცემა არ სჭირდება. Session::flush();

session დამხმარე

სესიებთან წვდომა შესაძლებელია session დამხმარე ფუნქციითაც. მაგალითად დავაბრუნოთ სესიაში შენახული key გასაღების მნიშვნელობა : dump(session('key')); თუ გვსურს, რომ session ფუნქციის მეშვეობით სესიაში შევიტანოთ ახალი მნიშვნელობები : session(['key' => 'value']);
16. მონაცემთა ვალიდაცია
მომხმარებლის მიერ სერვერზე გაგზავნილ ინფორმაციას აუცილებლად სჭირდება გადამოწმება ანუ ვალიდაცია. ამ თავში ვისაუბრებთ სწორედ ამ თემაზე.

პირველ რიგში 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'],
]);
            

ვალიდაციის შეწყვეტა პირველივე დარღვევისას

დავუშვათ ვალიდაციაში აღწერილი გვაქვს რამდენიმე წესი, მაგრამ გვინდა, რომ ვალიდაცია შეწყდეს წესების პირველივე დარღვევისას, ამისათვის წესების აღწერაში უნდა ჩავსვათ bail წესი :
$request->validate([
    'title' => 'bail|required|unique:posts|max:255',
    'body' => 'required',
]);
            
ამ შემთხვევაში თუ title ატრიბუტის unique წესი დაირღვევა, მაშინ max წესის გადამოწმება აღარ მოხდება.

ვალიდაციის შეცდომების გამოტანა

როდესაც ვალიდაციის წესები დაირღვევა, Laravel-ი მომხმარებელს ავტომატურად გადაამისამართებს წინა გვერდზე, დარღვევების შესახებ ინფორმაცია კი, ასევე ავტომატურად შეინახება სესიაში.

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

<!-- სიახლის დასამატებელი ფორმა -->
            

ვალიდაციის შეტყობინებების განსაზღვრა

სისტემაში არსებულ, ვალიდაციის ნებისმიერ წესს, შეესაბამება კონკრეტული შეტყობინება, ეს შეტყობინებები აღწერილია lang/en/validation.php ფაილში და შესაძლებლობა გვაქვს, ნებისმიერი მათგანი გადავაკეთოთ ჩვენი საჭიროებისამებრ.

@error დირექტივა

იმის დასადგენად ფიქსირდება თუ არა შეცდომა კონკრეტული ატრიბუტისათვის (მაგალითად სიახლის სათაურისათვის - title), შეგვიძლია გამოვიყენოთ @error დირექტივა :
<!-- /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
            

ფორმის ხელახალი შევსება

როდესაც მომხმარებელი ვალიდაციის წესების დარღვევით შეავსებს ფორმას და გაგზავნის მას, თუ ფორმა მარტივია და შედგება რამდენიმე ველისაგან, მომხმარებელს უბრალოდ გადავამისამართებთ ისევ ფორმის გვერდზე, მაგრამ თუ ფორმა რთულია და შეიცავს ძალიან ბევრ ველებს, მაშინ მომხმარებელს ამ ველების თავიდან შევსება მოუწევს, რაც არც თუ ისე მოსახერხებელია. ამ პრობლემის გადასაწყვეტად გამოიყენება request ობიექტის flash მეთოდი, რომელიც მოთხოვნის ტანში ჩადებულ ინფორმაციას ინახავს სესიაში : $title = $request->old('title'); ასევე შეგვიძლია გამოვიყენოთ გლობალური დამხმარე old : <input type="text" name="title" value="{{ old('title') }}">

სამომხმარებლო ვალიდატორი

იმისათვის რათა შევქმნათ საკუთარი ვალიდატორი, უნდა მივმართოთ Validator ფასადს და გამოვიყენოთ მისი მეთოდი - make, რომელიც ქმნის ვალიდატორის ობიექტს, ამ მეთოდს პარამეტრებად უნდა გადავცეთ შესამოწმებელი ინფორმაციის შემცველი მასივი და ვალიდაციის წესების შემცველი მასივი. პირველ პარამეტრს ანუ შესამოწმებელ ინფორმაციას, მოგვცემს მოთხოვნის ობიექტის - request-ის all() მეთოდი.
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. ფორმა გავაგზავოთ შემდეგი ვარიანტებით :
  • ორივე ველი დავტოვოთ ცარიელი და ისე დავაჭიროთ გაგზავნის ღილაკს
  • სათაურის ველში შევიყვანოთ მნიშვნელობა - '45', ტექსტი დავტოვოთ ცარიელი და ისე დავაჭიროთ გაგზავნის ღილაკს
ამ უკანასკნელ შემთხვევაში ვნახავთ, რომ title ველისათვის დაირღვევა ორი წესი : ის უნდა შეიცავდეს მხოლოდ ანბანის ასოებს და შეყვანილი მნიშვნელობის მიმიმალური სიგრძე უნდა იყოს 5.

იმისათვის რათა, კონკრეტული ველის ყველა შეცდომის შეტყობინება გამოვიტანოთ უნდა გამოვიყენოთ $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
            
17. მიგრაციები, მბ ვერსიათა კონტროლი, ცხრილების მოწყობა მბ-ში
ამ თავში განვიხილავთ მონაცემთა ბაზასთან (მბ) სამუშაო ინსტრუმენტებს.

მბ მიგრაცია

მიგრაცია არის ვერსიათა კონტროლის გამარტივებული სახე მონაცემთა ბაზებთან სამუშაოდ, მისი მეშვეობით ფრეიმვორკის კონსოლიდან შესაძლებელია მბს ცხრილების შექმნა, რედაქტირება და ა.შ. მიგრაციების გამოყენებას განსაკუთრებით დიდი მნიშვნელობა აქვს მაშინ, როდესაც ვმუშაობთ სხვა პროგრამისტებთან ერთად, ანუ გუნდური მუშაობისას. უფრო კონკრეტულად : თუ ჩვენ, ჩვენს ლოკალურ სივრცეში შევიტანთ ცვლილებებს მონაცემთა ბაზაში (დავამატებთ ახალ ცხრილს, წავშლით არსებულს, რომელიმე კონკრეტულ ცხრილში ჩავამატებთ ახალ ველს და ა.შ), ბუნებრივია ეს ცვლილებები შეცდომებს გამოიწვევს გლობალურ, ანუ რეალურ გარემოში ჩვენი კოდის ატვირთვისას ან სხვებისათვის გაზიარებისას, რადგანაც ამ გარემოებში არ იქნება ასახული ჩვენს მიერ გაკეთებული ცვლილებები. მიგრაცია კი არის ერთგვარი 'დოკუმენტაცია', რომელშიც აღწერილია ეს ცვლილებები და მისი დახმარებით, გუნდის სხვა წევრებს მარტივად შეუძლიათ თავიანთ გარემოშიც ასახონ თითოეული სიახლე.

მბ კონფიგურაცია

როგორც ვიცით მბს კონფიგურაციული პარამეტრები ინახება config/database.php ფაილში. ეს ფაილი აბრუნებს მასივს რომელშიც აღწერილია სხვადასხვა პარამეტრები. '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

ცხრილის წაშლა და სახელის შეცვლა

ცხრილის სახელის შესაცვლელად გამოიყენება Schema ფასადის rename მეთოდი :
use Illuminate\Support\Facades\Schema;

Schema::rename($from, $to);
            
ცხრილის წასაშლელად უნდა გამოვიყენოთ Schema ფასადის drop ან dropIfExists მეთოდი :
Schema::drop('users');

Schema::dropIfExists('users');
            

ანონიმური მიგრაციის კლასები

ლარაველის მე-9-ე ვერსიაში აღარ ხდება მიგრაციის კლასთა დასახელების განსაზღვრა, ამის ნაცვლად გამოიყენება ანონიმური კლასები. შევადაროთ მე-8-ე ვერსიის მიგრაციის ფაილი მე-9-ე ვერსიისას :
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');
    }
};
            
ახლა განვმარტოთ თუ რის გამო მოხდა ეს ცვლილება. განვიხილოთ ასეთი სიტუაცია :

  1. დავუშვათ ჩვენი აპლიკაციის 1.0 ვერსიაში გვქონდა 'create_news_table' მიგრაციით შექმნილი 'news' ცხრილი.
  2. აპლიკაციის 1.1 ვერსიაში შევქმენით მიგრაცია, რომელმაც წაშალა 'news' ცხრილი.
  3. აპლიკაციის 1.2 ვერსიაში ისევ გვინდა 'news' ცხრილის შექმნა, ამჯერად უკვე სხვა ველებითა და შინაარსით, ამისათვის ისევ გავაკეთეთ 'create_news_table' მიგრაცია.
ბუნებრივია მე-3-ე ბიჯზე შექმნილ მიგრაციაში აღწერილი კლასის დასახელება დაემთხვევა 1-ელ ბიჯზე შექმნილ მიგრაციაში აღწერილი კლასის დასახელებას. რაც შეცდომას გამოიწვევს მიგრაციების გაშვებისას ლარაველის მე-8-ე ვერსიაში, მე-9-ე ვერსიაში კი ეს აღარ მოხდება ანონიმური კლასების დახმარებთ.
18. ინფორმაციის შეტანა მბ-ში (Seeders)
ამ თავში განვიხილავთ ფრეიმვორკის მექანიზმს, რომელიც გამოიყენება მბ-ს საწყისი მონაცემებით შევსებისათვის. ეს ნექანიზმი აღწერილია შემდეგ ფაილში : database/seeders/DatabaseSeeder.php (ing: Seed წყარო, საწყისი). გარდა ამ ფაილში აღწერილი DatabaseSeeder კლასისა, შესაძლებელია რომ ჩვენც შევქმნათ ჩვენი საკუთარი კლასები, ფრეიმვორკის კონსოლის გამოყენებით. მაგალითისათვის შევქმნათ კლასი ArticlesSeeder, ამისათვის უნდა ავკრიფოთ ბრძანება : 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
19. მუშაობა მბ-სთან, ფასადი DB

ფასადი DB

ფასადი DB გამოიყენება მბ-სთან სამუშაოდ, იგი აღჭურვილია იმ მეთოდებით, რომლებიც შეიძლება დაგვჭირდეს ნებისმიერი ტიპის მოთხოვნის გასაშვებად მონაცემთა ბაზაში: select, update, insert, delete, statement.

DB::select

DB ფასადის select მეთოდი გამოიყენება მბს ცხრილიდან ინფორმაციის ამოსაღებად. მეთოდს პირველ პარამეტრად უნდა გადაეცეს მოთხოვნის შაბლონი, ტანი.
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;
}
            

DB::insert

DB ფასადის insert მეთოდი გამოიყენება მბს ცხრილში ინფორმაციის შესატანად :
$insert = DB::insert("INSERT INTO articles (name, text, img) VALUES(?,?,?)", ['Article 4','Article 4 Text','img4.jpg']);
dd($inser);
            

DB::update

DB ფასადის update მეთოდი გამოიყენება მბს ცხრილში ინფორმაციის განახლებისათვის :
$update = DB::update("UPDATE articles SET name=? WHERE id > ?", ['Renamed Article', 1]);
dd($update);
            
ეს მეთოდი აბრუნებს ზემოქმედებული ჩანაწერების რაოდენობას.

DB::delete

DB ფასადის delete მეთოდი გამოიყენება მბს ცხრილში ჩანაწერების წასაშლელად :
$delete = DB::delete("DELETE FROM articles WHERE id=?", [1]);
dd($delete);        
            
მეთოდი აბრუნებს წაშლილი ჩანაწერების რაოდენობას.

DB::statement

DB ფასადის statement მეთოდი გამოიყენება ისეთი ტიპის ბრძანებების შესასრულებლად, რომლებიც არ მიეკუთვნებიან არც ამორჩევითი ტიპის ბრძანებათა ოჯახს (select) და არც ცვლილებათა ტიპის ბრძანებათა ოჯახს (insert, delete, update). მაგალითად მბ-ცხრილის წასაშლელად ჩვენ დაგვჭირდება სწორედ statement მეთოდი.
$statement = DB::statement("DROP TABLE test");
dd($statement);  // true/false     
            

DB::unprepared

თუ გვსურს, რომ ბრძანება გავუშვათ არაწინასწარგანსაზღრული ფორმატით მაშინ უნდა გამოვიყენოთ DB ფასადის unprepared მეთოდი :
$unprepared = DB::unprepared('UPDATE articles SET text = "New text" WHERE id = 2');
dd($unprepared); // true/false       
            
გამომდინარე იქიდან, რომ unprepared მეთოდი არ იყენებს წინასწარგანსაზღრულ ფორმატსა და მიმაგრებულ პარამეტრებს, არსებობს SQL ინექციის დიდი საფრთხე. მომხმარებლის მიერ გამოგზავნილი ინფორმაცია ამ მეთოდით არასდროს უნდა შევინახოთ ბაზაში.
20. მბ მოთხოვნათა კონსტრუქტორი

ინგ : Builder - მწარმოებელი, მწარმოებელი-ქარხანა, მშენებელი

მოთხოვნათა კონსტრუქტორის უკან მოიაზრება სპეციალური კლასი Builder (ფსევდონიმი queryBuilder ანუ მოთხოვნათა მშენებელი :)) ), რომელსაც გააჩნია კონკრეტული მეთოდები, რომელთაგანაც თითოეული უზრუნველჰყოფს ბრძანების სრული ტანის კონკრეტული ნაწილის ფორმირებას, ჩვენ აღარ გვიწევს მოთხოვნის ხელით დაწერა. აღნიშნული კლასი აღწერილია vendor/laravel/framework/src/Illuminate/Database/Builder.php ფაილში.

იმისათვის რათა გამოვიყენოთ ეს კლასი, პირველ რიგში უნდა შევქმნათ მბ-ს ცარიელი მოთხოვნის ობიექტი კონკრეტული ცხრილისათვის, ამისათვის უნდა მივმართოთ DB ფასადის table მეთოდს, რომელსაც პარამეტრად უნდა გადავცეთ ცხრილის დასახელება.

get მეთოდი

ეს მეთოდი იღებს ყველანაირ ინფორმაციას კონკრეტული ცხრილიდან და აბრუნებს ობიექტთა მასივს :
$articles = DB::table('articles')->get();
dd($articles);          
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება "SELECT * FROM articles"

first მეთოდი

ეს მეთოდი იღებს პირველ ჩანაწერს კონკრეტული ცხრილიდან და აბრუნებს stdClass კლასის ობიექტის სახით:
$article = DB::table('articles')->first();
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "SELECT * FROM articles LIMIT 1"

value მეთოდი

ეს მეთოდი იღებს ინფორმაციას ერთი რომელიმე კონკრეტული ველიდან, პარამეტრად უნდა გადაეცეს ველის დასახელება. მეთოდი აბრუნებს სტრიქონული ტიპის შედეგს, აგრეთვე უნდა აღინიშნოს, რომ ეს მეთოდიც, ისევე როგორც წინა, ინფორმაციას იღებს მხოლოდ პირველი ჩანაწერიდან.
$name = DB::table('articles')->value('name');
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "SELECT 'name' FROM articles LIMIT 1"

pluck მეთოდი

ეს მეთოდი იღებს ინფორმაციას ერთი რომელიმე კონკრეტული ველიდან, პარამეტრად უნდა გადაეცეს ველის დასახელება. მეთოდი აბრუნებს ველის მნიშვნელობათა მასივს, აღსანიშნავია რომ, pluck მეთოდი, value მეთოდისაგან განსხვავებით, ინფორმაციას იღებს ყველა ჩანაწერიდან. (ინგ: Pluck - კრეფა, აღება, აკრეფა, შეგროვება).
$names = DB::table('articles')->pluck('name');
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "SELECT 'name' FROM articles"

count მეთოდი

ეს მეთოდი თვლის ჩანაწერების რაოდენობას ცხრილში და აბრუნებს რიცხვითი ტიპის მნიშვნელობას :
$count = DB::table('articles')->count();
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "SELECT COUNT(*) FROM articles"

max მეთოდი

ეს მეთოდი აბრუნებს მაქსიმალურ მნიშვნელობას განსაზღვრული ველისათვის, პარამეტრად უნდა გადაეცეს ველის დასახელება :
$max = DB::table('articles')->max('id');
            
ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "SELECT MAX('id') FROM articles" ასევე არსებობს min, avg და sum მეთოდებიც, რომლებიც ანალოგიურად მუშაობს.

select მეთოდი

ამ მეთოდის გამოყენება ხელსაყრელია მაშინ, როდესაც გვსურს კონკრეტული ველების ამოღება ჩანაწერებიდან და არა ყველა ველისა. ველთა დასახელებები მეთოდს უნდა გადაეცეს პარამეტრებად, შესაძლებელია მასივის სახით გადაცემა ან თითოეული ველის დასახელების გადაცემა ცალკე პარამეტრად :
$articles = DB::table('articles')->select('id','name');
            
თუ ამ ბრძანებას გავუშვებთ, შედეგად დაგვიბრუნდება Builder კლასის ობიექტი და არა ის შედეგი რაც გვსურს, იმიტომ რომ select მეთოდს ჯერ არ შეუსრულებია თავისი საქმე, ამისათვის მას უნდა მივაშველოთ get მეთოდი :
$articles = DB::table('articles')->select('id','name');
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "SELECT id, name FROM articles"

where მეთოდი

ხშირად საჭიროა ინფორმაციის ამოღება გარკვეული ფილტრების მიხედვით (მაგ: ამოირჩეს ჩანაწერები სადაც id მეტია 2-ზე), ასეთი სახის ბრძანებების შესასრულებლად select მეთოდთან ერთად უნდა გამოვიყენოთ where მეთოდი. როგორც ვიცით where ოპერატორის სინტაქსი სტანდარტულ PHP-ში შემდეგნაირია : 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();   
            

whereBetween მეთოდი

$articles = DB::table('articles')->whereBetween('id',[2,5])->get();   
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "SELECT 'name' FROM articles WHERE id BETWEEN 2 AND 5" ამ მეთოდის შებრუნებული მეთოდია whereNotBetween.

ამ ორი მეთოდიას ანალოგიურია მეთოდები whereIn და whereNotIn ამიტომ მათ მაგალითებს აღარ მოვიყვანთ.

groupBy მეთოდი

ეს მეთოდი გამოიყენება ცხრილის ჩანაწერთა დაჯგუფებისათვის, პარამეტრად უნდა გადაეცეს იმ ველის დასახელება რომლის მიხედვითაც ვაჯგუფებთ :
$articles = DB::table('articles')->get()->groupBy('name');
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "SELECT * FROM articles GROUP BY name"

take მეთოდი

ეს მეთოდი დაგვეხმარება მაშინ თუ გვსურს ამოღებული ჩანაწერების რაოდენობის ლიმიტირება :
$articles = DB::table('articles')->take(2)->get();
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "SELECT * FROM articles LIMIT 2"

insert მეთოდი

ეს მეთოდი გამოიყენება მბს ცხრილში ინფორმაციის შესატანად, მას პარამეტრად უნდა გადაეცეს ის ინფორმაცია, რომლის შეტანაც გვსურს ცხრილში :
$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 მეთოდი

ეს მეთოდი გამოიყენება მბს ცხრილში ინფორმაციის განახლებისათვის. ლოგიკურია, რომ მეთოდის გამოყენება ხდება where მეთოდთან ერთად, რადგან თუ ვუშვებთ ბრძანებას რომელიც სისტემას ეუბნება, რომ ჩაატაროს განახლების ოპერაცია მაშინ ისიც უნდა მივუთითოთ თუ რა უნდა განაახლოს :
$update = DB::table('articles')->where('id',2)->update(['name' => 'hello world']);
             
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "UPDATE articles SET name='hello world' WHERE id=2" მეთოდი აბრუნებს ზემოქმედებული ჩანაწერების რაოდენობას.

delete მეთოდი

ეს მეთოდი გამოიყენება მბს ცხრილში ინფორმაციის წასაშლელად. ამ მეთოდის გამოყენებაც where მეთოდთან ერთად ხდება, რადგან თუ ვუშვებთ წაშლის ბრძანებას მაშინ ისიც უნდა მივუთითოთ თუ რა და სად უნდა წაიშალოს :
$delete = DB::table('articles')->where('id',2)->delete();
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "DELETE FROM articles WHERE id=2"

find მეთოდი

თუ გვსურს ერთი ჩანაწერის ამორება id ველის მეშვეობით, უნდა გამოვიყენოთ find მეთოდი :
$article = DB::table('articles')->find(3);
            
ამ ჩანაწერმა რეალურად შექმნა შემდეგი ბრძანება : "SELECT * FROM articles WHERE id = 3" მეთოდი აბრუნებს ზემოქმედებული ჩანაწერების რაოდენობას.

join მეთოდი

$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();
            

leftJoin მეთოდი

$users = DB::table('users')->leftJoin('posts', 'users.id', '=', 'posts.user_id')->get();    
            

დაუმუშავებელი განაცხადები

ზოგჯერ შეიძლება დაგვჭირდეს, რომ ბრძანებებში ჩავრთოთ, მოთხოვნათა კონსტრუქტორის ჩვეული სტილისაგან განსხვავებული, არასდანტარტული გამოსახულებები. დავარქვათ მათ დაუმუშავებელი გამოსახულებები. ამისათვის გამოიყენება DB ფასადის raw მეთოდი (ინგ: Raw Data - საწყისი, დაუმუშავებელი მონაცემები).
 $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`
            
ეს ყველაფერი მოგვცემდა ასეთ შედეგს :



დაუმუშავებელი გამოსახულებები მოთხოვნაში შედის სტრიქონის სახით და ამიტომ მათი გამოყენებისას უკიდურესად ფრთხილად უნდა ვიყოთ რათა თავი დავიცვათ SQL ინექციებისაგან.

JSON ველები

რომელიმე სატესტო ცხრილში გავაკეთოთ JSON ტიპის ველი 'properties' და ასევე ცხრილში შევიტანოთ რამდენიმე ჩანაწერი properties ველების შემდეგი მნიშვნელობებით :
{
    "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();
            

WHERE ფილტრის დამატებითი ფუნქციონალი

whereBetween / orWhereBetween

$users = DB::table('users')->whereBetween('id', [1, 100])->get();
            

whereNotBetween / orWhereNotBetween

$users = DB::table('users')->whereNotBetween('id', [1, 100])->get();
            

whereIn / whereNotIn / orWhereIn / orWhereNotIn

$users = DB::table('users')->whereIn('id', [1, 2, 3])->get();

$users = DB::table('users')->whereNotIn('id', [1, 2, 3])->get();
            

whereNull / whereNotNull / orWhereNull / orWhereNotNull

$users = DB::table('users')->whereNull('updated_at')->get();

$users = DB::table('users')->whereNotNull('updated_at')->get();
            

whereDate / whereMonth / whereDay / whereYear / whereTime

$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();
            

whereColumn / orWhereColumn

whereColumn მეთოდის მეშვეობით შეგვიძლია დავადგინოთ ტოლია თუ არა ორი სხვადასხვა ველის მნიშვნელობები :
$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();
            
21. მონაცემთა მოდელი
როგორც ვიცით, ფრეიმვორკი laravel-ი დაფუძნებულია შაბლონ mvc-ზე (Model, View, Controller ანუ მოდელი, წარმოდგენა, კონტროლერი). ამ თავში ვისაუბრებთ მოდელებზე. იმის შესახებ თუ რა არის მოდელი და რა ევალება მას, ვისაუბრეთ მე-3-ე თავში, ახლა კი მოვიყვანოთ შემდეგი განმარტება: მოდელი არის მბ-ს ცხრილში შეტანილი კონკრეტული ჩანაწერების აბსტრაქცია, რომელიც მბს ცხრილის ელემენტებს წარმოგვიდგენს ობიექტების სახით, მარტივი სიტყვებით მოდელი არის განსაზღვრული კლასის ობიექტი, რომლის მეთოდებითაც ხდება ცხრილის მონაცემებთან მუშაობა.

მოდელის შსაქმნელად კონსოლში უნდა ავკრიფოთ შემდეგი ბრძანება :

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

თვისება $table

$table თვისება არის მოდელის ობიექტის დახურული თვისება, რომელშეიც უნდა განისაზღვროს მბს იმ ცხრილის სახელი, რომელთანაც ვმუშაობთ მოცემულ მოდელში :
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    protected $table = 'articles';
}
            
უნდა აღინიშნოს, რომ ამ შემთხვევაში $table თვისების განსაზღვრა საჭირო არ არის, რადგანაც ცხრილისა და მოდელის სახელებს შორის გვაქვს ის დამოკიდებულება რაც ზემოთ ვთქვით : მოდელს სახელად აქვს ცხრილის დასახელების მხოლობითი ფორმა. ასეთ შემთხვევაში კი სისტემა ავტომატურად მიხვდება, თუ რომელ ცხრილთან უნდა იმუშავოს.

თვისება $primaryKey

$primaryKey თვისება არის მოდელის ობიექტის დახურული თვისება, რომელშეიც უნდა განისაზღვროს მბს ცხრილის პირველადი გასაღები ველის სახელი, რომელიც გამოიყენება ჩანაწერთა იდენტიფიკაციისათვის, თუ ამ თვისებაში არაფერს განვსაზღვრავთ მაშინ ფრეიმვორკი გაარკვევს შეიცავს თუ არა ცხრილი ველს სახელად id და ჩათვლის, რომ ისაა პირველადი გასაღები და აგრეთვე ავტოგადამთვლელი (auto_increment), ხოლო თუ გვინდა, რომ პირველადი გასაღები იყოს დავუშვათ ველი article_id მაშინ ეს უნდა განვსაზღვვროთ შემდეგნაირად :
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    protected $primaryKey = 'article_id';
}
            

თვისება $incrementing

ეს ღია თვისება გამოიყენება იმის განსასაზღვრავად არის თუ არა რომელიმე ველი ავტოგადამთვლელი :
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    public $incremeting = FALSE;
}
            

თვისება $timestamps

ეს ღია თვისება გამოიყენება იმის განსასაზღვრავად შეივსოს თუ არა ცხრილის created_at და updated_at ველები ავტომატურად ცხრილთან მუშაობისას. როგორც ვიცით, მიგრაციით ცხრილის შექმნისას ფრეიმვორკი ამ ველებს ავტომატურად ამატებს ცხრილში.
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    public $timestamps = FALSE; // ამ შემთხვევაში ეს ველები შეივსება მნიშვნელობა Null-ით
}
            

თვისება $fillable

ეს დახურული თვისება გამოიყენება მბს ცხრილის იმ ველთა განსასაზღვრავად, რომლებშიც შესაძლებელია, რომ მომხმარებელმა შეიტანოს ინფორმაცია :
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    protected $fillable = ['name','text','img'];
}
            

თვისება $guarded

ეს დახურული თვისება გამოიყენება მბს ცხრილის იმ ველთა განსასაზღვრავად, რომლებშიც მომხმარებელს არ შეუძლია ინფორმაციის შეტანა :
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;
}   

... 
            






ამ მიდგომას ეწოდება ORM - Object Relational Mapping და არის ერთგვარი ხიდი რელაციურ ბაზებსა და ოოპ ობიექტებს შორის.

რასაკვირველია მოდელში შესაძლებელია მთხოვნათა კონსტრუქტორის გამოყენებაც :

$a$articles = Article::where('id', '>', 2)->orderBy('name')->take(3)->get();
            

კონკრეტული მოდელის ამოღება

კონკრეტული მოდელის ამოსაღებად გამოიყენება find, first და firstWhere მეთოდები, რომლებიც კოლექციის ნაცვლად აბრუნებენ ერთ ობიექტს.
// მოდელის ამოღება პირველადი გასაღების მიხედვით (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 'ვერ მოიძებნა';

});     
            

findOrFail() მეთოდი

ზოგჯერ საჭიროა, რომ იმ შემთხვევაში, თუ მოდელი ვერ მოიძებნება, დავაფიქსიროთ კონკრეტული გამონაკლისი. findOrFail და firstOrFail მეთოდები შეეცდებიან სასურველი მოდელის პოვნას და თუ ეს არ მოხერხდა მაშინ შეგვიძლია :
  • ჩვენს კონტროლერში დავაფიქსიროთ Illuminate\Database\Eloquent\ModelNotFoundException.php ფაილში აღწერილი ინსტრუქციების მიხედვით დაგენერირებული გამონაკლისი :
    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
                }
            }
        }
    }    
                        
  • გადავმისამართდეთ 404 გვერდზე (სწორედ ამას აკეთებს სისტემა ავტომატურად თუ პირველ ვარიანტს არ განვიხილავთ).

ინფორმაციის შეტანა მბს ცხრილში

firstOrCreate() მეთოდი

ამ მეთოდის გამოყენება ძალიან ხელსაყრელია მაშინ თუ გვინდა, რომ ცხრილში დავამატოთ უნიკალური ინფორმაცია განსაზღვრული ველისათვის. ამ მეთოდსაც პარამეტრად უნდა გადაეცეს მასივში აღწერილი ველთა დასახელებები და შესაბამისი მნიაშვნელობები :
$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', თუ ესეთი ჩანაწერი არსებობს მაშინ მეთოდი დააბრუნებს ამ ჩანაწერის მოდელს. ხოლო თუ არ არსებობს ასეთი ჩანაწერი, მაშინ მეთოდი შექმნის მას და დააბრუნებს ახალშექმნილ მოდელს.

save() მეთოდი

იმისათვის რათა ცხრილში ინფორმაცია შევიტანოთ save მეთოდით, პირველ რიგში უნდა შევქმნათ ცარიელი მოდელის ობიექტი, შემდეგ კი უბრალოდ მივმართოთ და განვუსაზღვროთ მნიშვნელობები შექმნილი ობიექტის იმ თვისებებს, რომელთა დასახელებებიც ემთხვევა ცხრილის ველთა დასახელებებს :
$article = new Article;

$article->name = 'ტესტი';
$article->text = 'ტესტი';
$article->img  = 'ტესტი';

$article->save();
            
save მეთოდით ჩანაწერების დამატებისას ავტომატურად ხდება ცხრილის created_at და updated_at ველების შევსება.

create() მეთოდი

ეს მეთოდი გამოიყენება ცხრილში ინფორმაციის შესატანად :
$article = Article::create([
    'name' => 'სათაური',
    'text' => 'ტექსტი',
    'img' => 'test.jpg',
]);    
            
create() მეთოდით ჩანაწერების შექმნისას მოდელში აუცილებლად უნდა გვქონდეს აღწერილი მასიურ განსაზღვრებადობასთან დაკავშირებული fillable და guarded თვისებები :
protected $fillable = ['name', 'text','img'];  
            
ან :
protected $fillable = [];  
            

ინფორმაციის განახლება მბს ცხრილში

ინფორმაციის განახლებისათვის, ჯერ უნდა ამოირჩეს კონკრეტული ჩანაწერი, შემდეგ კი უბრალოდ თავიდან უნდა განვსაზღროთ სასურველი ველების მნიშვნელობები, ბოლოს კი გამოვიძახოთ ისევ მეთოდი save() :
$article = Article::find(3);
$article->name = "ახალი სათაური";
$article->save();
            
updated_at ველის მნიშვნელობა ავტომატურად განახლდება.

მასიური განახლებები

შესაძლებელია, რომ ერთ ჯერზე განვაახლოთ არა მარტო კონკრეტული ჩანაწერი, არამედ რამდენიმე ჩანაწერიც ერთდროულად :
Article::where('id','>',1)->update([
    'name' => 'ახალი სათიურები',
    'text' => 'ახალი ტექსტები'
]);
            

Delete() მეთოდი

ეს მეთოდი შლის ჩანაწერებს მბს ცხრილიდან, მაგრამ იმისათვის რათა ცხრილის რომელიმე ჩანაწერი წაიშალოს, ჯერ უნდა ამოირჩეს ეს ჩანაწერი :
$article = Article::find(4);
$article->delete();
            

destroy() მეთოდი

ამ მეთოდის მეშვეობით შესაძლებელია ჩანაწერების წაშლა ამოურჩევლად, მას პარამეტრად უნდა გადაეცეს წასაშლელი ჩანაწერის იდენტიფიკატორი (id)
  Article::destroy(9);
            
თუ რამდენიმე ჩანაწერის წაშლა გვსურს ერთდროულად, მაშინ მეთოდს პარამეტრად უნდა გადაეცეს მასივი სადაც შეტანილი იქნება ამ ჩანაწერთა იდენტიფიკატორები.

softDelete

ეს მეთოდი გამოიყენება ჩანაწერთა წასაშლელად, იმ განსხვავებით, რომ იგი რეალურად არ შლის ჩანაწერებს, მისი მუშაობის პრინციპი წააგავს ოპერაციულ სისტემაში ფაილების წაშლის პრინციპს, რის შედეგადაც ეს ფაილები ხვდება სანაგვე ყუთში. ამ მეთოდის გამოსაყენებლად ცხრილს უნდა დავამატოთ ახალი ველი deleted_at, რომელშიც შეინახება ჩანაწერის წაშლის მომენტის შესაბამისი დროის ნიშნული. ამისათვის გავაკეთოთ ახალი მიგრაცია :
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();
            
22. მბ-ს ცხრილების ურთიერთდამოკიდებულებები (hasmany, belongsto, ...)
ამ თავში ვისაუბრებთ მბს ცხრილების ურთიერთკავშირებზე. როგორც ვიცით, ცხრილების შექმნისას შესაძლებელია მათი ერთმანეთთან დაკავშირება კონკრეტული ველების გამოყენებით (foreign key).

მაგალითად გვაქვს სტატიების ცხრილი articles და იმ მომხმარებლების ცხრილი - users, რომლებიც ამატებენ ამ სტატიებს. ასეთ შემთხვევაში, როგორც წესი, მომხმარებლის საიდენტიფიკაციო ნომერი (id) იწერება ხოლმე articles ცხრილის user_id ველში. ანუ articles ცხრილის user_id ველი არის users ცხრილთან კავშირის საგარეო გასაღები. პრაქტიკაში ასეთი კავშირები საკმაოდ ხშირია და ამიტომ laravel-ში ჩადგმულია ფუნქციონალი, რომელიც ამარტივებს ამ კავშირებთან მუშაობას.





laravel-ში არსებობს ცხრილთა შორის კავშირის რამდენიმე ვარიანტი, მათ შორის ძირითადებია :

  • ერთი ერთთან
  • ერთი ბევრთან
  • ბევრი ბევრთან

კავშირი 'ერთი ერთთან', hasOne() მეთოდი

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
            
ვიხილავთ ამდაგვარ შეტყობინებას :



ეს იმიტომ, რომ სისტემამ მიგრაციის ძველ ფაილებსაც მიაკითხა და შესაბამისი შეტყობინებაც დააბრუნა - კონკრეტული ცხრილები უკვე არსებობსო. ამ პრობლემის მოგვარების რამდენიმე ვარიანტი არსებობს, ყველაზე მარტივია შევქმნათ database/migrations/old საქაღალდე, მასში გადავიტანოთ ყველა ძველი მიგრაცია და ისე გავუშვათ '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
            

კავშირი 'ერთი მრავალთან', hasMany() მეთოდი

ამ მომენტისათვის ჩვენ უკვე გვაქვს მომხმარებლებისა და სიახლეების ცხრილები - users და articles. დავაკავშიროთ ისინი შემდეგნაირად: სიახლეების ცხრილს დავამატოთ ერთი ველი - 'user_id', რომელშიც ჩაიწერება სიახლეების ავტორების იდენტიფიკატორები.
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-თან დაკავშირდება კავშირის ტიპით - ერთი მრავალთან.



user მოდელში ჩავამატოთ ახალი მეთოდი 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 ობიექტს :



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;
            

sql ბრძანებათა ოპტიმიზაცია, ე.წ 'ძუნწი ჩატვირთვა'

როგორც ვიცით შემდეგი კოდის გაშვებეისას :
$articles = Article::all();
dump($articles);
            
შესრულდება შემდეგი ბრძანება და ბრუნდება მოდელების შემდეგი კოლექცია :


დავუშვათ გვსურს დავბეჭდოთ ავტორის სახელი თითოეული სიახლისათვის :
$articles = Article::all();
        
foreach($articles as $article)
{
    echo $article->user->name . '<br>';
}
            
ასეთ შემთხვევაში მივიღებთ შემდეგ სურათს :



როგორც ვხედავთ ციკლის ყოველ იტერაციაზე შესრულდა ერთი და იგივე sql ბრძანება, ეს დიდი პრობლემა არაა, როდესაც ბაზაში სულ რამდენიმე ჩანაწერი გვაქვს, მაგრამ თუ იქ 10 000 ჩანაწერია, ამ ბრძანების გაშვება საკმაოდ დიდი დატვირთვა იქნება მბ-ს სერვერისათვის, რაც, დიდი ალბათობით, პრობლემებს შექმნის.

ასეთ შემთხვევაში გამოიყენება ე.წ 'ძუნწი ჩატვირთვა' :)) ამ შემთხვევაში დამატებითი ინფორმაციის ჩატვირთვა ხდება კოლექციასთან ერთად. ოპტიმიზირებული ჩატვირთვისას უნდა გამოვიყენოთ მეთოდი with, რომელიც უზრუნველჰყოფს ინფორმაციის ჩატვირთვას ძირითად ცხრილთან დაკავშირებული სხვა ცხრილებიდანაც. მეთოდს პარამეტრად უნდა გადაეცეს იმ მოდელის დასახელება, რომელთან დაკავშირებაც გვსურს :

$articles = Article::with('user')->get();

foreach($articles as $article)
{
    echo $article->user->name . '<br />';
}
            
ასეთ შემთხვევაში მივიღებთ შემდეგ სურათს :



ანუ სულ გაეშვა ორი ბრძანება : პირველმა მოახდინა კოლექციის გენერირება, მეორემ კი ჩატვირთა ამ კოლექციასთან დაკავშირებული საჭირო ინფორმაციები. ბრძანებათა ამდაგვარ ოპტიმიზაციას ეწოდება 'ძუნწი ჩატვირთვა' :))

მეთოდი has()

დავუშვათ გვინდა ამოვარჩიოთ ისეთი მომხმარებლები, რომლებსაც დამატებული აქვთ ერთი სიახლე მაინც, ასეთ შემთხვევაში გამოიყენება has მეთოდი :
$users = User::has('articles')->get();

foreach($users as $user)
{
    echo $user->name . '<br />';
}        
            




ანუ : User კლასის has მეთოდს პარამეტრად გადაეცა იმ მოდელის დასახელება, რომლის შესაბამის ცხრილშიც გვსურს User კოლექციის შესაბამისი ჩანაწერების არსებობა/არარსებობის დადგენა.

has მეთოდთან ერთად შესაძლებელია სხვადასხვა პირობითი ოპერატორების გამოყენებაც, დავუშვათ გვინდა ამოვარჩიოთ ის მომხმარებლები, რომლებსაც ორზე მეტი სიახლე აქვთ დამატებული:

$users = User::has('articles','>=', 2)->get();

foreach($users as $user)
{
    echo $user->name . '<br />';
}        
            




კავშირი 'ბევრი ბევრთან', belongsToMany() მეთოდი

ცხრილებს შორის კავშირის ეს ვარიანტი განვიხილოთ მომხმარებლებისა და მათი როლების მაგალითზე. მომხმარებელს შეიძლება გააჩნდეს რამდენიმე როლი (ადმინისტრატორი, სტუმარი ...), მეორეს მხრივ კი ერთი როლიც შეიძლება დაკავშირებული იყოს რამდენიმე მომხმარებელთან. ამდაგვარი კავშირის ასაღწერად დაგვჭირდება სამი ცხრილი : მომხმარებლების ცხრილი (users), როლების ცხრილი (roles) და მომხმარებლებისა და როლების საიდენტიფიკაციო ნომრების ცხრილი (role_user). მომხმარებლების ცხრილი უკვე გვაქვს, შევქმნათ დანარჩენი ორის შესაბამისი მიგრაციები : 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' ცხრილთან სამუშაო მოდელის დასახელება.

გავუშვათ ამ მიგრაციების შესრულების ბრძანება და შევქმნათ ცხრილები.







ახლა შევქმნათ "roles" ცხრილის შესაბამისი მოდელი 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) და თუ ასე არ მოვიქცეოდით დაფიქსირდებოდა კონფლიქტი, შეცდომა.

ზუსტად ანალოგიურად მოხდება უკუკავშირის დამყარება : "ერთი როლი უკავშირდება რამდენიმე მომხმარებელს".

save(), saveMany() და create() მეთოდები

$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' => 'მეოთხე სიახლის ტექსტი'
]);      
            

update() მეთოდი

მეთოდს პარამეტრად უნდა გადაეცეს მბ-ს ცხრილის იმ ველთა დასახელებების შემცველი მასივი, რომელთა შეცვლაც გვსურს :
$user = User::find(1);

$user->articles()->where('id', 3)->update([
    'name' => 'მესამე სიახლის ახალი სათაური'
]);
            
ამ ჩანაწერში უნდა გავითვალისწინოთ ერთი რამ : როგორც ვხედავთ ჯერ ვიღებთ მომხმარებლის შესახებ ინფორმაციას (id=1) და შემდეგ ვუშვებთ ინფორმაციის განახლების შესახებ ბრძანებას. თუ სიახლე რომლის შეცვლაც გვსურს (id=3), არ ეკუთვნის ამორჩეულ მომხმარებელს, მაშინ სისტემა არ განაახლებს ამ ჩანაწერს, და ეს ასეც უნდა იყოს.
23. მომხმარებელთა აუტენტიფიკაცია
აპლიკაციების შექმნისას ხშირად აუცილებელია, რომ აპლიკაციის კონკრეტული ნაწილი დაიმალოს საერთო თვალთახედვიდან (მაგალითად ადმინისტრატორის პანელი) და მასზე ხელი მიუწვდებოდეს მხოლოდ რეგისტრირებულ მომხმარებლებს, ამისათვის გამოიყენება მომხმარებელთა აუტენტიფიკაციის სისტემა, რომელიც ჩაშენებულია ფრეიმვორკ ლარაველის სტანდარტულ ფუნქციონალში.

რა არის აუტენტიფიკაცია ?

აუტენტიფიკაცია ეს არის მომხმარებელთა გადამოწმების პროცესი, რომლის დროსაც დარდება მომხმარებლის მიერ აკრეფილი ინფორმაცია (მაგ: სახელი და პაროლი), სისტემაში არსებულ მონაცემებთან. თუ მოხდა ამ ინფორმაციათა დამთხვევა შეიძლება ითქვას, რომ მოხდა მომხმარებელის აუტენტიფიცირება, მაგრამ ეს კიდევ არ ნიშნავს იმას, რომ მას აქვს სისტემაში რაიმეს გაკეთების უფლება. აუტენტიფიკაციის ლოგიკური გაგრძელებაა ავტორიზაცია.

რა არის ავტორიზაცია ?

ავტორიზაცია არის აუტენტიფიცირებული მომხმარებლის უფლებათა გადამოწმების პროცესი.

აუტენტიფიკაციის პარამეტრები განსაზღვრულია config/auth.php ფაილში. მომხმარებლის აუტენტიფიკაციის პროცესი შედგება ორი ძირითადი ელემენტისაგან: პირველი ეს არის ე.წ მცველი - guard, მეორე კი - პროვაიდერი provider.

მცველი განსაზღვრავს თუ რა სახით იქნას შენახული ინფორმაცია იმ მომხმარებლის შესახებ, რომელიც აკეთებს მოთხოვნას, ანუ როგორ შევინახოთ ინფორმაცია იმის შესახებ, რომ მოთხოვნა გააკეთა მაგალითად აუტენტიფიცირებულმა მომხმარებელმა, ეს ინფორმაცია შესაძლებელია შენახულ იქნას სესიაში ან სპეციალურ სტრიქონში სახელად token.

პროვაიდერი განსაზღვრავს თუ როგორ და რა სახით შეიძლება მიიღოს მომხმარებელმა ინფორმაცია მბ-დან ან სხვა წყაროდან.

შ აუტენტიფიკაციის სისტემასთან მუშაობისთვის საჭიროა წარმოდგენის ფაილები, მარშრუტები და კონტროლერები. არსებობს ამ ყველაფრის შექმნის რამდენიმე ვარიანტი. განვიხილოთ ერთ-ერთი მათგანი - Laravel Breeze

რა არის Laravel Breeze ?

Laravel Breeze არის პაკეტი, რომელიც საშუალებას გვაძლევს შევქმნათ ყველა აუცილებელი კომპონენტი, აპლიკაციაში აუტენტიფიკაცის სისტემის ჩასაშენებლად. წარმოდგენის ფაილები იქმნება Laravel შაბლონიზატორისა და Tailwind CSS-ის დახმარებით. Tailwind CSS არის CSS ფრეიმვორკი, რომელიც მოიცავს სხვადასხვა კლასებისა და სელექთორების საკმაოდ ფართო ნაკრებს, მასში ასევე აღწერილია ამ კლასების შესაბამისი სტილები და ჩვენ ისღა დაგვრჩენია უბრალოდ მოვარგოთ ეს კლასები ჩვენს აპლიკაციას.

Laravel 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,
    
    ...
];

...
            

Auth ფასადი

user() მეთოდი

აუტენტიფიცირებული მომხმარებლის შესახებ ინფორმაციის მისაღებად უნდა მივმართოთ Auth ფასადს :
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());
    }
}
            

check() მეთოდი

იმის დასადგენათ არის თუ არა აუტენტიფიცირებული მომხმარებელი, რომელიც აკეთებს HTTP მოთხოვნას, გამოიყენება Auth ფასადის check მეთოდი :
use Illuminate\Support\Facades\Auth;

if(Auth::check())
{
    echo 'აუტენტიფიცირებულია';
}
else
{
    return redirect()->route('login');
}
            
მიუხედავად იმისა, რომ ამ მეთოდით შესაძლებელია მომხმარებლის აუტენტიფიცირება/არააუტენტიფიცირების გადამოწმება, უკეთესი ვარიანტია თუ საჭირო მარშრუტებს შუამავლების მეშვეობით დავიცავთ ხოლმე და თუ მომხმარებელი ამ შუამავალს გაივლის, შესაბამისად აღარც იმის გადამოწმება დაგვჭირდება არის თუ არა იგი აუტენტიფიცირებული.

დაცული მარშრუტები

რიგ შემთხვევებში საჭიროა, რომ კონკრეტულ მარშრუტებთან წვდომა შეეძლოთ მხოლოდ აუტენტიფიცირებულ მომხმარებლებს. როგორც ზემოთ აღვნიშნეთ, ამისათვის გამოიყენება auth შუამავალი :
Route::get('/profile', function () {

    // მხოლოდ აუტენტიფიცირებული მომხმარებლები

})->middleware('auth');
            

არააუტენტიფიცირებული მომხმარებლების გადამისამართება

როდესაც auth შუამავალი დაადგენს, რომ მომხმარებელი არააუტენტიფიცირებულია, იგი მას გადაამისამართებს მარშრუტზე სახელად - auth. შეგვიძლია შევცვალოთ ეს მიდგომა, თუ ჩავერევით app/Http/Middleware/Authenticate.php შუამავლის redirectTo მეთოდში :
protected function redirectTo($request)
{
    return route('somewhere');
}
            

აუტენტიფიკაციის სამომხმარებლო სისტემა

როგორც ადრე აღვნიშნეთ, არსებობს აუტენტიფიკაციის სისტემის შექმნის რამდენიმე ვარიანტი და განვიხილეთ ერთ-ერთი მათგანი - Laravel Breeze. რა თქმა უნდა შესაძლებელია, რომ შევქმნათ ჩვენი საკუთარი სისტემაც. ამაში დაგვეხმარება, ისევ და ისევ, Auth ფასადი :
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])) 
{
    // წარმატებული აუტენტიფიკაცია
}
            

მომხმარებლების დამახსოვრება

ალბათ ხშირად შეგვხვედრია აუტენტიფიკაციის ფორმები, რომლებსაც თან ახლავს 'დამიმახსოვრე' ღილაკი (remember me). იმისათვის რათა აპლიკაციაში გამოვიყენოთ ეს ფუნქციონალი attempt მეთოდს უნდა გადავცეთ ლოგიკური ტიპის დამატებითი პარამეტრი. true მნიშვნელობის შემთხვევაში სისტემა მომხმარებლის შესახებ ინფორმაციას სესიაში შეინახავს მანამ, სანამ თავად მომხმარებელი არ დააჭერს სისტემიდან გამოსვლის ღილაკს (logout). users ცხრილს თუ დავაკვირდებით, ვნახავთ, რომ იგი შეიცავს remember_token ველს. სწორედ ამ ველში შეინახება დამახსოვრებული მომხმარებლის შესაბამისი უნიკალური სტრიქონი.
use Illuminate\Support\Facades\Auth;

$remember = $request->has('remember');

if (Auth::attempt(['email' => $email, 'password' => $password], $remember)) 
{
    // მომხმარებელი დამახსოვრებულია ...
}
            
როდესაც მომხმარებელი აუტენტიფიკაციისას აწვება 'დამიმახსოვრე' ღილაკს, იკვრება მოქმედებათა შემდეგი ჯაჭვი : გენერირდება უნიკალური ჰეშირებული სტრიქონი, რომელიც ინახება როგორც ბრაუზერში (Cookie-ს სახით), ასევე სერვერზე ('users' ცხრილის 'remember_token' ველში), ამის შემდეგ თუ მომხმარებელი დახურავს და ისევ გახსნის ბრაუზერს, სისტემა გადაამოწმებს არსებობს თუ არა დამახსოვრებული ჰეშ-სტრიქონი Cookie-ში, თუ კი - მაშინ შეამოწმებს შეესაბამება თუ არა ეს სტრიქონი ბაზაში არსებულ რომელიმე ჩანაწერს და თუ ეს ასეა მაშინ მომხმარებელი ავტომატურად ხდება აუტენტიფიცირებული. Cookie-ში შენახული ინფორმაცია იარსებებს მანამ, სანამ მომხმარებელი არ გამოვა სისტემიდან (logout), უბრალოდ ბრაუზერის დახურვით ეს ინფორმაცია არ იშლება.

კონკრეტული მომხმარებლის ობიექტის აუტენტიფიკაცია

დავუშვათ გვაქვს app/Models/User კლასის კონკრეტული ობიექტი და გვსურს, რომ ამ ობიექტის შესაბამისი მომხმარებელი გახდეს აუტენტიფიცირებული სისტემაში. ამისათვის უნდა მივმართოთ Auth ფასადის login მეთოდს :
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 მეთოდს :
Auth::loginUsingId(1, $remember = true);
            

სისტემიდან გამოსვლა

იმისათვის რათა სესიიდან წაიშალოს ინფორმაცია აუტენტიფიცირებული მომხმარებლის შესახებ და შესაბამისად მომხმარებელიც გამოვიდეს სისტემიდან, უნდა მივმართოთ Auth ფასადის logout მეთოდს. რეკომენდებულია, რომ სისტემიოდან გამოსვლის შემდეგ მოვახდინოთ სესიის გასუფთავება და CSRF თოქენის ხელახალი გენერირება :
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

public function logout(Request $request)
{
    Auth::logout();

    $request->session()->invalidate();

    $request->session()->regenerateToken();

    return redirect('/');
}
            
24. მომხმარებელთა ავტორიზაცია
როგორც უკვე აღვნიშნეთ, მომხმარებელთა ავტორიზაცია არის პროცესი, რომლის დროსაც ხდება მომხმარებლის ხელმისაწვდომობის, უფლების ქონა/არქონის გადამოწმება ამა თუ იმ მოქმედებაზე. Laravel-ში შესაძლებელია შეიქმნას კონკრეტული ინსტრუმენტები, რომლებიც განსაზღვრავენ ამ ხელმისაწვდომობას. ამ ინსტრუმენტებს პირობითად დავარქვათ 'კონტროლიორები'.

როგორც წესი, კონტროლიორების განსაზღვრა ხდება ხოლმე 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)) 
{
    // მომხმარებელს არ შეუძლია სიახლის არც წაშლა და არც რედაქტირება ... 
}
            
25. ლოკალიზაცია (localization, lang)
ფრეიმვორკ laravel-ში გვხვდება, ტექსტების სხვადასხვა ენაზე გადათრგმნისათვის საჭირო, საკმაოდ მოსახერხებელი ფუნქციები. მათი დახმარებით მარტივად ხდება ჩვენი აპლიკაციის გამართვა რამდენიმე ენაზე ერთდროულად. ენობრივი სტრიქონები (სათარგმნი სტრიქონები) ინახება lang საქაღალდეში. აქვე უნდა შევქმნათ ქვე-საქაღალდეები თითოეული იმ ენისათვის, რომელზეც გვსურს ჩვენი პროექტის გადათარგმნა:
/lang
    /en
        messages.php
    /ka
        messages.php
            
ფრეიმვორკის დაინსტალირების შემდეგ ამ საქაღალდეში, ნაგულისხმეობის პრინციპით, იქმნება ლოკალიზაციის ერთადერთი საქაღალდე ინგლისური ენისათვის lang/en.

როგორც წესი, თითოეული ლოკალიზაცია ინახება საქაღალდეში, რომლის დასახელებაც ემთხვევა შესაბამისი ენის კოდს (მაგ: ინგლისური - en, ქართული - ka და ა.შ). ლოკალიზაციის ქვე-საქაღალდეებში ინახება ე.წ ფაილი ლექსიკონები, მათში აღწერილია კონკრეტული სტრიქონების თარგმანები კონკრეტულ ენაზე. ამ სტრიქონებს ეწოდებათ ენობრივი კონსტანტები. ფაილ ლექსიკონებში ბრუნდება უბრალო ასოციაციური მასივები, რომლის გასაღებებიც გადასათარგმნი სტრიქონებია, ხოლო ამ გასაღებთა მნიშვნელობები - ამ სტრიქონების თარგმანები შესაბამის ენაზე.

თითოეული ფაილი ლექსიკონი განკუთვნილია, პროექტის კონკრეტული ელემენტისათვის, მაგალითად ვალიდაციის ელემენტს აქვს თავისი ფაილი (validation.php), გვერდების გადანომრვის ელემენტს - თავისი (pagination.php), აუტენტიფიკაციის გვერდს - თავისი (auth.php) და ა.შ.

ლოკალიზაციისა და ფაილ-ლექსიკონის შექმნა

lang საქაღალდეში შევქმნათ ახალი ლოკალიზაცია ქართული ენისათვის : lang/ka. ამ ლოკალიზაციაში უნდა ჩავაკოპიროთ ის სტანდარტული ფაილ-ლექსიკონები, რომლებსაც ფრეიმვორკი ავტომატურად ქმნის ინსტალაციისას lang/en ლოკალიზაციაში, ეს ფაილებია : validation.php, pagination.php, auth.php და passwords.php. ამის გაკეთება აუცილებელია, იმდენად რამდენადაც, აოლიკაციის მუშაობისას, დიდი ალბათობით დაგვჭირდება იმ ელემენტებთან მუშაობა, რომლებთანაც ეს ლექსიკონებია დაკავშირებული.

ამის შემდეგ უბრალოდ უნდა გავხსნათ ახლადშექმნილ ლოკალიზაციაში არსებული ლექსიკონ-ფაილები და მათში აღწერილ მასივებში, გასაღებების მნიშვნელობები მივუთითოთ ქართულად.

ეს რაც შეეხებოდა უკვე არსებულ ლექსიკონ-ფაილებს. ახლა შევქმნათ საკუთარი ლექსიკონ-ფაილი და ვნახოთ თუ როგორ ხდება მისი გამოყენება, lang/ka საქაღალდეში შევქმნათ ფაილი - messages.php :
return [
    'welcome' => 'კეთილი იყოს თქვენი მობრძანება',
    'hello' => 'მოგესალმებით'
];
            
ლოკალიზაციის შექმნის შემდეგ საჭიროა config/app.php ფაილში აღწერილი ასოციაციური მასივის locale გასაღების მნიშვნელობის ჩასწორება : 'locale' => 'ka' ამავე მასივის fallback_locale გასაღებში მითითებულია ალტერნატიული ლოკალიზაციის დასახელება : 'fallback_locale' => 'en' ანუ თუ locale გასაღებში მითითებული ლოკალიზაცია რაიმე მიზეზის გამო მიუწვდომელი იქნება სისტემისათვის, მაშინ ფრეიმვორკი შეეცდება გამოიყენოს ალტერნატიული ლოკალიზაცია.

Lang ფასადი

Lang ფასადი შექმნილია ლოკალიზაციებთან და ფაილ-ლექსიკონებთან სამუშაოდ. ფასადს აქვს მეთოდი get, რომლის მეშვეობითაც ხდება საჭირო ენობრივი კონსტანტის თარგმანის მიღება :
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' => 'ვასო'));
}
            

მიმდინარე ლოკალიზაციის განსაზღვრა

მიმდინარე ლოკალიზაციის დასადგენად გამოიყენება App ფასადის currentLocale და isLocale მეთოდები :
use Illuminate\Support\Facades\App;

$locale = App::currentLocale();

if (App::isLocale('ka')) 
{
    //
}
            
26. ტესტირებაზე ორიენტირებული დეველოპმენტი (TDD)

ვებ-პროგრამირების სფეროში მომუშავე ნებისმიერ ადამიანს, ალბათ ერთხელ მაინც ექნება ყური მოკრული ამდაგვარი გამონათქვამებისათვის :

  • ხარისხიანი პროდუქტი (პროექტი) ტესტების გარეშე არ არსებობს.
  • ჯერ ტესტები, მერე კოდი ...
  • ტესტებთან თუ გიმუშავიათ ? (ეს ალბათ უფრო გასაუბრებებზე :) )

დასაწყისში ამ ყველაფრის შემდეგ ჩნდება საპასუხო კითხვები :

  • კი, მაგრამ მე ვწერ პროექტებს ტესტების გარეშე და ეს პროექტები მუშობენ ...
  • რა გავტესტო თუ კოდი არ მაქვს ?! ..
  • რას გულისხმობს ტესტებთან მუშაობა ? რას ვტესტავთ ? რატომ ვტესტავთ ?

ამ თავში შევეცდებით პასუხი გავცეთ ამ კითხვებს, შემდეგ ორ თავში კი შევქმნით ტესტირებაზე ორიენტირებულ ორ პროექტს შემდეგი დასახელებებით :

  • ვინილების შესაკვეთი აპლიკაცია (MVC)
  • დავალებების ბლოკნოტი (REST API)

რა არის ტესტირებაზე ორიენტირებული დეველოპმენტი ?

ტესტირებაზე ორიენტირებული დეველოპმენტი (TDD - Test-driven development) არის პროგრამული უზრუნველყოფის შექმნის პროცესი, რომელიც დაფუძნებულია, სატესტო ფუნქციებად ანუ სატესტო შემთხვევებად (test cases) დაფორმატებულ კონკრეტულ პროგრამულ ამოცანებზე და რომლის დროსაც ეს დაფორმატება ხდება პროგრამის საბოლოო იერსახის შექმნამდე.

კი, გეთანხმებით - ცოტა ჩახლართული განმარტებაა :)) მოვიყვანოთ კონკრეტული მაგალითი: დავუშვათ ვქმნით პროექტს, რომლის მეშვეობითაც უნდა შეგვეძლოს მონაცემთა ბაზაში ჩავწეროთ, განვაახლოთ და წავშალოთ ინფორმაციები სხვადასხვა პროდუქტის შესახებ, ანუ გვაქვს სტანდარტული CRUD სისტემა. უფრო კონკრეტულად კი გვაქვს შემდეგი ამოცანები :

  • პროდუქტის შექმნა (create)
  • პროდუქტის წაკითხვა (read)
  • პროდუქტის განახლება (update)
  • პროდუქტის წაშლა (delete)

იმისათვის, რომ ეს პროექტი გავხადოთ ტესტირებაზე ორიენტირებული, ბუნებრივია უნდა შევქმნათ ამ ამოცანებთან დაკავშირებული ტესტები, ანუ მოვახდინოთ ამ ამოცანების ტესტებად აღწერა. მაშ ასე, შევქმნათ შემდეგი ტესტები :

  • test_create_product
  • test_read_product
  • test_update_product
  • test_delete_product

უშუალოდ ტესტების შინაარსს რაც შეეხება, ამ მომენტისათვის მხოლოდ ზოგადი მონახაზის აღწერით შემოვიფარგლოთ : კონკრეტულ ტესტში უნდა მოვახდინოთ ამ ტესტთან დაკავშირებული ფუნქციის, ანუ პროექტის კონკრეტული ამოცანის შესრულების 'სიმულაცია' : მაგ: test_create_product ტესტში უნდა მივმართოთ პროდუქტის შექმნის მარშრუტს, შემდეგ გადავცეთ მას შესანახი ინფორმაცია, შემდეგ მივმართოთ შესაბამის მოდელს და ა.შ.

ამის შემდეგ სისტემას უნდა ვუთხრათ, რომ შეასრულოს ჩვენი ტესტები. თუ გავითვალისწინებთ, რომ ამ დროისათვის კოდის არცერთი ხაზი გვაქვს დაწერილი, სისტემა ბუნებრივია გვეტყვის :

  • 'მეგობარო, შენ არ გაქვს გაკეთებული პროდუქტის შესაქმნელი მარშრუტი და გააკეთე იგი'

ვუჯერებთ სისტემას, ვქმნით მარშრუტს და ხელახლა ვუშვებთ ტესტირების ბრძანებას, ამჯერად სისტემა გვეუბნება შემდეგს :

  • 'მარშრუტი კი შექმენი, მაგრამ კონტროლერიც გვჭირდება'


ფოტო აღებულია ამ სტატიიდან

და ა.შ, ნაბიჯ-ნაბიჯ მივუყვებით ფუნქციონალის გამართვას ანუ კოდის წერას. ეს პროცესი გარანტიაა იმისა, რომ ფუნქციონალის სწორად მუშობისათვის საჭირო არცერთი ფრაგმენტი გამოგვრჩება. გარდა ამისა, მას შემდეგ რაც ყველა საჭირო ტესტს აღვწერთ, მხოლოდ და მხოლოდ ერთი ბრძანების მეშვეობით შეგვეძლება, პროექტის სრული ფუნქციონალის გატესტვა, რასაც, თავის მხრივ, აქვს შემდეგი დადებითი მხარეები :

  • წარმოიდგინეთ, რომ თქვენ ხართ პროგრამული უზრუნველყოფის ტესტერი. თქვენ შეგეძლებათ უმარტივესად გადაამოწმოთ სწორად მუშაობს თუ არა ყველაფერი და ამისათვის არ დაგჭირდებათ კოდში ჩახედვა.
  • წარმოიდგინეთ, რომ თქვენ ხართ პროგრამისტი. რაიმე ხარვეზის არსებობის შემთხვევაში, თქვენ გექნებათ უზუსტესი ნიშნული იმისა თუ კოდის რომელი ფრაგმენტი არ მუშაობს, ეს კი, თავის მხრივ, გაზრდის თქვენს პროდუქტიულობას.
  • წარმოიდგინეთ, რომ თქვენ ხართ პროდუქტის მფლობელი. თქვენ შეგეძლებათ უსწრაფესად ადაპტირდეთ ხარვეზების აღმოჩენისა და აღმოფხვრის პროცესებში, რაც დაზოგავს თქვენს დროსა და ფინანსებს.

რა არის ფრაგმენტული ტესტირება ?

ინგ: Unit - ერთეული, ნაწილი, აგრეგატი, სექცია, ნასკვი, კვანძი;

ფრაგმენტული ტესტირება (unit testing) არის პროგრამული უზრუნველყოფების ტესტირების ტიპი, რომელშიც ხდება პროგრამის ინდივიდუალური ერთეულების, ფრაგმენტების ტესტირება. მისი მიზანია დამტკიცდეს, რომ პროგრამული კოდის თითოეული ფრაგმენტი გამართულად მუშობს. ფრაგმენტული ტესტები იწერება დეველოპერის მიერ, პროექტზე მუშაობის პროცესში. ფრაგმენტის უკან შეიძლება მოიაზრებოდეს კონკრეტული ფუნქცია, მეთოდი, მოდული და ა.შ

რა არის PHPunit ?

PHPunit არის PHP-ისათვის შექმნილი, ფრაგმენტული ტესტირების ფრეიმვორკი. მისი ავტორია Sebastian bergmann . PHPunit-ის მეშვეობით დეველოპერს მარტივად შეუძლია იპოვოს ხარვეზები კოდის კონკრეტულ ფრაგმენტებში შეტანილ ბოლო ცვლილებებში და დარწმუნდეს იმაში, რომ კოდის სხვა ნაწილებში ყველაფერი გამართულად მუშაობს.

ფოტო აღებულია ამ სტატიიდან

ტესტირება ლარაველში

PHP-ში დაწერილი პროგრამული უზრუნველყოფებისათვის არსებობს ტესტირების სხვადასხვა ხელსაწყოები : PHPunit, Mockery, PHPSpec, Storyplayer ... ლარაველში ჩაშენებულია PHPunit. დავაკვირდეთ composer.json ფაილის შემდეგ ფრაგმენტს :


phpunit.xml ფაილი

PHPunit-ის მუშაობისათვის საჭირო კონფიგურაციული პარამეტრები აღწერილია პროექტის ძირ საქაღალდეში არსებულ - phpunit.xml ფაილში. განვიხილოთ იგი.

<phpunit> ელემენტი

bootstrap ატრიბუტი

ამ ატრიბუტში განსაზღვრულია ავტოჩამტვირთველი ფაილი, რომელში აღწერილი ინსტრუქციებიც შესრულდება ტესტების გაშვებამდე.

colors ატრიბუტი

შესაძლო მნიშვნელობები: true ან false (ნაგულისხმები: true)

ატრიბუტი გასაზღვრავს გაფერადდეს თუ არა ტექსტი, როდესაც PHPUnit ტესტირების შედეგებს გამოგვიტანს.

ამ ატრიბუტის მნიშვნელობად 'true'-ს განსაზღვრა იგივეა, რომ ტესტის გაშვებისას ბრძანებათა ველში ბრძანებას მივაწეროთ შემდეგი ჩანაწერი '--colors=auto'

თუ გვსურს, რომ კონკრეტული ტესტის გაშვებისას ფერები არ იქნას გამოყენებული ბრძანებას უნდა მივაწეროთ '--colors=never' ჩანაწერი.

<testsuites> ელემენტი

ინგ : Suite - ნაკრები, კომპლექტი;

ეს ელემენტი შეიძლება იყოს ერთი ან რამდენიმე testsuite ელემენტის მშობელი.

<testsuite> ელემენტი

<testsuite> ელემენტს უნდა გააჩნდეს name ატრიბუტი და შესაძლოა ასევე ჰყავდეს ერთი ან რამდენიმე <directory> და/ან <file> შვილობილი ელემენტი, რომლებიც განსაზღვრავენ იმ ფაილებსა და/ან დირექტორიებს, რომლებსაც სისტემა მოძებნის ტესტების გაშვებისას. ფაქტიურად აქ აღწერილია ტესტების ფიზიკური მისამართები.

<coverage> ელემენტი

ინგ : Coverage - ხელის მოვლება, დაზღვევა, მოქმედების სფერო, გამოკვლევის მხარე, სადაზღვეო დაფარვა, სადაზღვეო დაცვა;

კომპიუტერულ მეცნიერებებში არსებობს ასეთი ცნება - კოდის დაზღვეულობა (code coverage), რომელიც განსაზღვრავს პროგრამული კოდის გატესტვის ხარისხს ტესტების კონკრეტულ ნაკრებთან მუშაობისას. რაც მეტია ეს ხარისხი, მით უფრო ნაკლებია შანსი იმისა, რომ პროგრამაში დაფიქსირდეს ხარვეზები, სხვა სიტყვებით თუ ვიტყვით - რაც უფრო მასშტაბურად იტესტება კოდი, მით უფრო საიმედოა იგი.

PHPUnit-ისათვის <coverage> ელემენტი არის ერთგვარი ფილტრი, რომლის მეშვეობითაც იგი ხვდება თუ რომელი და რა ტიპის ფაილები უნდა მოხვდეს კოდის დაზღვევის არეში.

<include> ელემენტი

განსაზღვრავს ფაილთა ჯგუფს, რომელიც უნდა მოხვდეს კოდის დაზღვევის არეში.

<directory> ელემენტი

<include> ელემენტის შვილობილი <directory> ელემენტი განსაზღვრავს იმ საქაღალდეებსა და ქვესაღალდეებს, რომლებში არსებული ფაილებიც უნდა მოხვდეს კოდის დაზღვევის არეში.

suffix ატრიბუტი

შესაძლო მნიშვნელობები: სტრიქონული ტიპის (ნაგულისხმები: '.php')

განსაზღვრავს სუფიქსზე დაფუძნებულ ფილტრს საქაღლდეებსა და ქვესაღალდეებში არსებულ, კოდის დაზღვევის არეში მოხვედრილი ფაილებისათვის.

<php> ელემენტი

ამ ელემენტში ხდება PHP-ს კონფიგურაციული პარამეტრების, კონსტანტების და გლობალური ცვლადების აღწერა.

<env> ელემენტი

ამ ელემენტში ხდება მნიშვნელობების შენახვა სუპერგლობალურ $_ENV მასივში.

tests საქაღალდე

ლარაველის პროექტის ძირ საქაღალდეში შექმნილია tests საქაღლდე, რომელიც მოიცავს ორ ქვესაქაღალდეს : Feature და Unit. Unit საქაღალდეში ინახება ფრაგმენტული ტესტები, რომელთა შესახებაც ჩვენ უკვე ვისაუბრეთ. Feature საქაღალდეში კი ინახება ე.წ ფუნქციონალური ტესტები, იმისათვის რათა გავარკვიოთ რას ნიშნავს ფუნქციონალური ტესტი, ვუყუროთ ამ პატარა ვიდეოს :

ხელის საშრობიც და ნაგვის ყუთიც ცალ-ცალკე მშვენივრად ასრულებს თავის საქმეს (წარმოვიდგინოთ, რომ მათ უკან მოიაზრება კონკრეტული ფუნქციის კონკრეტული მეთოდები, რომლებიც უკვე გატესტილი გვაქვს ფრაგმენტული ტესტირების გზით (unit tests)). მაგრამ, როდესაც მათ ერთად ვათავსებთ, რაღაც რიგზე ვერაა :))

სწორედ ეს იდეა დევს ფუნქციონალურ ტესტებშიც - იმის ნაცვლად, რომ აპლიკაციის კონკრეტული ასპექტები ვტესტოთ, ვახდენთ იმ მთლიანი ოპერაციის იმიტაციას, რომელიც შეიძლება, რომ მომხმარებელმა ჩვენს აპლიკაციაში განახორციელოს.

პროექტის ინსტალაციის შემდეგ ავტომატურად იქმნება ერთი ფრაგმენტული და ერთი ფუნქციონალური ტესტი, ერთი და იგივე დასახელებით - ExampleTest. ტესტების გაშვება შესაძლებელია რამდენიმენაირად :

php artisan test
            
vendor/bin/phpunit
            
Windows ოპერაციული სისტემის ბრძანებათა ველის შემთხვევაში :
vendor\bin\phpunit
            

ტესტის შექმნა

ტესტის შესაქმნელად უნდა გამოვიყენოთ php artisan-ის make:test ბრძანება :
php artisan make:test UserTest
            
ამ ბრძანებით შექმნილი ტესტი შეინახება tests/Feature საქაღალდეში.

თუ გვსურს, რომ ტესტი tests/Unit საქაღალდეში შეიქმნას, make:test ბრძანებას უნდა მივამატოთ --unit ჩანაწერი :

php artisan make:test UserTest --unit
            

HTTP ტესტები

HTTP მოთხოვნის შექმნა

ტესტში HTTP მოთხოვნის შესაქმნელად გამოიყენება get, post, put, patch და delete მეთოდები, თუმცა აღსანიშნავია, რომ ეს მეთოდები რეალურ HTTP მოთხოვნებს არ აგზავნიან, ისინი უბრალოდ ამ მოთხოვნების სიმულირებას ახდენენ. პასუხად კი, ძირითადი Illuminate\Http\Response კლასის ეგზემპლიარის ნაცვლად Illuminate\Testing\TestResponse კლასის ეგზეპლიარი ბრუნდება, რომელსაც საკმაოდ ბევრი მტკიცებითი ტიპის თვისებები გააჩნია (assertions).
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);
    }
}
            
ტესტი წარუმატებელი იქნება და მივიღებთ ამდაგვარ შედეგს :



და ეს ლოგიკურია : განვახორციელეთ მიმართვა არარსებულ მარშრუტზე და შევეცადეთ დაგვემტკიცებინა, რომ პასუხს აქვს სტატუსი 200. არარსებულ მისამართზე მიკითხვისას მიღებულ პასუხს, როგორც ცნობილია, აქვს სტატუსი 404, 404 კი არ უდრის 200-ს.

გარდა assertStatus მეთოდისა, ლარაველში გვხვდება საკმაოდ ბევრი მტკიცებითი ტიპის მეთოდები, რომლებსაც ეტაპობრივად გავეცნობით (ინგ: Assert - მტკიცება, განცხადება, დაცვა, პრეტენზია).

ცნობისათვის: ტესტებთან მუშაობისას CSRF შუამავალი ავტომატურად გამორთულია.

სესია / აუტენტიფიკაცია

HTTP ტესტირების დროს მოთხოვნისთვის სესიის გადაცემა შესაძლებელია ამდაგვარად :
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('/');
    }
}
            

JSON API-ს ტესტირება

JSON API-ს ტესტირება შესაძლებელია შემდეგი მეთოდების მეშვეობით: json, getJson, postJson, putJson, patchJson, deleteJson და optionsJson. ბუნებრივია შესაძლებელია, რომ ამ მეთოდებს გადავცეთ სხვადასხვა სახის ინფორმაციები.

დავწეროთ ტესტი, რომელიც /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']);
            

წარმოდგენის ფაილების ტესტირება

ლარაველის ტესტებში წარმოდდგენის ფაილების დაგენერირება შესაძლებელია HTTP მოთხოვნის სიმულაციის გარეშე. ამისათვის გამოიყენება view მეთოდი, რომელსაც აუცილებელ პარამეტრად უნდა გადაეცეს წარმოდგენის ფაილის დასახელება და ასევე შეიძლება მიემაგროს დამატებითი ინფორმაცია, მეორე - არააუცილებელი პარამეტრის სახით. მეთოდი დააბრუნებს Illuminate\Testing\TestView კლასის ეგზემპლიარს, რომელსაც გააჩნია შემდეგი მეთოდები : assertSee, assertSeeInOrder, assertSeeText, assertSeeTextInOrder, assertDontSee და assertDontSeeText.
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
ამტკიცებს, რომ პასუხს აქვს მოცემული ქუქი :
$response->assertCookie($cookieName, $value = null);
            
assertCookieExpired
ამტკიცებს, რომ პასუხს აქვს მოცემული ქუქი და ამ ქუქის ვადა ამოწურულია :
$response->assertCookieExpired($cookieName);
            
assertCookieNotExpired
ამტკიცებს, რომ პასუხს აქვს მოცემული ქუქი და ამ ქუქის ვადა არ არის ამოწურული :
$response->assertCookieNotExpired($cookieName);
            
assertCookieMissing
ამტკიცებს, რომ პასუხს არ აქვს მოცემული ქუქი :
$response->assertCookieMissing($cookieName);
            
assertCreated
ამტკიცებს, რომ პასუხს აქვს HTTP სტატუს-კოდი 201 :
$response->assertCreated();
            
assertForbidden
ამტკიცებს, რომ პასუხს აქვს HTTP სტატუს-კოდი 403 :
$response->assertForbidden();
            
assertJson
მეთოდი პასუხს აფორმატებს მასივად და ასევე ამტკიცებს, რომ იგი შეიცავს მოცემულ JSON ინფორმაციას :
$response->assertJson(array $data, $strict = false);
            
assertNoContent
ამტკიცებს, რომ პასუხს აქვს არგუმენტად გადაცემული სტატუს-კოდი და არ აქვს შიგთავსი :
$response->assertNoContent($status = 204);
            
assertNotFound
ამტკიცებს, რომ პასუხს აქვს HTTP სტატუს-კოდი 404 :
$response->assertNotFound();
            
assertOk
ამტკიცებს, რომ პასუხს აქვს HTTP სტატუს-კოდი 200 :
$response->assertOk();
            
assertRedirect
ამტკიცებს, რომ პასუხი წარმოადგენს გადამისამართებას :
$response->assertRedirect($uri);
            
assertRedirectToSignedRoute
ამტკიცებს, რომ პასუხი წარმოადგენს კონკრეტულ მარშრუტზე გადამისამართებას :
$response->assertRedirectToSignedRoute($uri);
            
assertSessionHas
ამტკიცებს, რომ სესია შეიცავს მოცემულ ინფორმაციას :
$response->assertSessionHas($key, $value = null);
            
assertSessionHasErrors
ამტკიცებს, რომ სესია შეიცავს ვალიდაციის შეცდომებს მოცემული მნიშვნელობებისათვის :
$response->assertSessionHasErrors(['name', 'email']);
            
assertSessionHasNoErrors
ამტკიცებს, რომ სესია არ შეიცავს ვალიდაციის შეცდომებს :
$response->assertSessionHasNoErrors();
            
assertStatus
ამტკიცებს, რომ პასუხს აქვს მოცემული HTTP სტატუს-კოდი :
$response->assertStatus($code);
            
assertSuccessful
ამტკიცებს, რომ პასუხს აქვს წარმატებული (>= 200 and < 300) HTTP სტატუს-კოდი :
$response->assertSuccessful();
            
assertUnauthorized
ამტკიცებს, რომ პასუხს აქვს HTTP სტატუს-კოდი 401 :
$response->assertUnauthorized();
            
assertViewHas
ამტკიცებს, რომ პასუხად მიღებულ წარმოდგენის ფაილს მიმაგრებული აქვს კონკრეტული ინფორმაცია :
$response->assertViewHas($key, $value = null);
            
assertViewHasAll
ამტკიცებს, რომ პასუხად მიღებულ წარმოდგენის ფაილს მიმაგრებული აქვს ინფორმაციების კონკრეტული სია :
$response->assertViewHasAll(array $data);
            
assertViewIs
ამტკიცებს, რომ პასუხად მიღებულ წარმოდგენის ფაილი დაბრუნდა კონკრეტული მარშრუტის მიერ :
$response->assertViewIs($value);
            

აუტენტიფიკაციის მტკიცებითი მეთოდები

assertAuthenticated
ამტკიცებს, რომ მომხმარებელი აუტენტიფიცირებულია :
$this->assertAuthenticated($guard = null);
            
assertGuest
ამტკიცებს, რომ მომხმარებელი არააუტენტიფიცირებულია :
$this->assertGuest($guard = null);
            
assertAuthenticatedAs
ამტკიცებს, რომ სპეციფიური მომხმარებელია აუტენტიფიცირებული :
$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 ტრეიტის გამოყენებისას, ტესტების ყოველ გაშვებაზე ასევე სრულდება მიგრაციებში აღწერილი ინსტრუქციებიც.

მოდელმწარმოებლები

ტესტის შესრულებამდე შეიძლება საჭირო იყოს ბაზაში რამდენიმე ჩანაწერის დამატება. იმის ნაცვლად, რომ ყოველ ჯერზე ხელით ვამატოთ კონკრეტული ველების კონკრეტული მნიშვნელობები, შეგვიძლია გამოვიყენოთ ე.წ მოდელმწარმოებლები (model factories, ინგ: Factory - ფაბრიკა, ქარხანა, საწარმო). განვიხილოთ მოდელმწარმოებლის სტრუქტურა კონკრეტული მაგალითის საფუძველზე :
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 - გაყალბება, ფალსიფიცირება, მოტყუება, თაღლითობა :))) ).

Faker ბიბლიოთეკისათვის (მატყუარა ბიბლიოთეკისავის :D) ენის განსაზღვრა შესაძლებელია config/app.php ფაილში აღწერილი faker_locale გასაღების მეშვეობით.

დავუბრუნდეთ ისევ ტესტებს. ტესტში მოდელმწარმოებლის გამოყენება შესაძლებელია შემდეგნაირად :

use App\Models\User;
 
public function test_models_can_be_instantiated()
{
    $user = User::factory()->create();
 
    // ...
}
            

ხელმისაწვდომი მტკიცებითი მეთოდები

assertDatabaseCount

ამტკიცებს, რომ მონაცემთა ბაზის ცხრილში არის კონკრეტული რაოდენობის ჩანაწერი :
$this->assertDatabaseCount('users', 5);
            

assertDatabaseHas

ამტკიცებს, რომ მონაცემთა ბაზის ცხრილში არსებობს კონკრეტული გასაღები/მნიშვნელობა წყვილის შესაბამისი ჩანაწერი :
$this->assertDatabaseHas('users', [
    'email' => 'vasil.nadiradze@gmail.com',
]);
            

assertDatabaseMissing

ამტკიცებს, რომ მონაცემთა ბაზის ცხრილში არ არსებობს კონკრეტული გასაღები/მნიშვნელობა წყვილის შესაბამისი ჩანაწერი :
$this->assertDatabaseMissing('users', [
    'email' => 'vasil.nadiradze@gmail.com',
]);
            
მტკიცებითი მეთოდების სრული სია შეგიძლიათ იხილოთ აქ.
27. ტესტსირებაზე ორიენტირებული MVC აპლიკაცია

ამ თავში გავაკეთებთ ვინილის შესაკვეთ, ტესტსირებაზე ორიენტირებულ MVC აპლიკაციას შემდეგი საბაზისო ფუნქციონალით :

  • ვინილის ძებნა
  • ვინილის დამატება კალათში
  • ვინილის შეკვეთა

მიმოხილვა

პროექტში ვიმუშავებთ შემდეგ თემებსა და საკითხებზე :

  • ტოდ (TDD)
  • ტესტების წერა სამფაზიანი შაბლონით
  • მტკიცებები (Assertions)
  • კოდის ოპტიმიზაცია
ბრძანებათა ველის მეშვეობით გადავიდეთ სასურველ დირექტორიაში და დავაინსტალიროთ პროექტი :
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

ამის შემდეგ დავწეროთ ტესტი, რომელიც შეამოწმებს გვაქვს თუ არა ყველა საჭირო ინფორმაცია ძებნის გვერდზე, ანუ გვაქვს თუ არა პროდუქტის ჩამონათვალი. სწორედ აქ ერთვება საქმეში ტესტების სამფაზიანი ზოგადი სტრუქტურა, ეს ფაზებია :

  • მომზადება
  • ქმედება
  • მტკიცება

მომზადების ფაზა

პირველ რიგში საჭიროა, რომ შევამზადოთ 'სამყარო', რომელშიც ჩვენი ტესტი იმუშავებს. ეს ხშირად გულისხმობს ინფორმაციის შენახვას ბაზაში ან სესიაში და ა.შ ჩვენს შემთხვევაში დაგვჭირდება MySQL მონაცემთა ბაზა, რომელშიც დავამატებთ სხვადასხვა ვინილებს. ტესტში შევაიმპორტოთ პროდუქტის, ანუ ვინილების მოდელი და ჩავამატოთ ახალი მეთოდი მომზადების ფაზითურთ :
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']);
            

***

წინა ტესტში კი დავამტკიცეთ, რომ ინფორმაცია ნამდვილად მიმაგრებულია წარმოდგენის ფაილზე, მაგრამ არ გადაგვიმოწმებია ხედავს თუ არა მომხმარებელი ამ ინფორმაციას, ჩვენ ხომ წარმოდგენის ფაილში არც გამოგვიტანია ეს ინფორმაცია. ამაში დაგვეხმარება assertSeeInOrder მეთოდი. ჩავამატოთ ახალი ტესტი :
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 ფაილში არსებული ეს კონფიგურაციული პარამეტრები :

იმისათვის რათა ტესტებიც კორექტულად და წესისამებრ გაეშვას და ბრაუზერშიც ვადევნოთ თვალი არსებულ სიტუაციას, ამ კონკრეტულ მომენტში მოვიქცეთ ასე :

  1. მოვხსნათ კომენტარები ზედა ფოტოზე მითითებულ კონფიგურაციულ პარამეტრებს.
  2. ტესტებში გამოვიყენოთ RefreshDatabase ტრეიტი.

კალათის ტესტი

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). ამის შემდეგ კი გვაქვს სამი სხვადასხვა მტკიცება :

  • assertRedirect - ამტკიცებს, რომ ფორმის გაგზავნის შემდეგ სერვერი გადაგვამისამართებს მთავარ გვერდზე.
  • assertSessionHasNoErrors - ამტკიცებს, რომ სესიაში არ დაფიქსირებულა ვალიდაციის შეცდომები.
  • assertSessionHas - ამტკიცებს, რომ სესიაში შეინახა კონკრეტული ინფორმაცია (cart მასივი).

ტესტის გაშვების შემდეგ, სისტემა ჯერ გვეტყვის, რომ არ გვაქვს მარშრუტი, შემდეგ კონტროლერი და მისი მეთიოდი. შევქმნათ ისინი, routes/web.php ფაილში ჩავამატოთ შემდეგი ჩანაწერები :
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'));
    }
}
            

კალათის კოდის დახვეწა

კალათის კონტროლერს თუ დავაკვირდებით შევამჩნევთ, რომ ბევრგან ვიღებთ ინფორმაციას სესიიდან. მოვახდინოთ ამ ოპერაციის ინკაფსულაცია სერვისში. app საქაღალდეში შევქმნათ Services საქაღალდე, მასში კი ფაილი CartService.php შემდეგი კოდით :
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');
    }
}
            

შეკვეთის კოდის დახვეწა

შეკვეთის კონტროლერის კოდის დასახვეწადაც დაგვჭირდება პატარა დახმარება ახლადშექმნილი სერვისისაგან ^_^, თუმცა მანამდე CartService სერვისის get მეთოდში ჩავამატოთ ჯამური თანხის ველი :
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
            
28. ტესტსირებაზე ორიენტირებული REST API აპლიკაცია

შესავალი

ამ თავში გავაკეთებთ დავალებათა წიგნაკის REST API-ს, რომელიც, რა თქმა უნდა, იქნება ტესტირებაზე ორიენტირებული. მუშობის პროცესში შევეხებით შემდეგ საკითხებს :

  • დასაწყისი
    • პროექტის ინსტალაცია და გამართვა
    • ჩვენი პირველი API ტესტი
  • მონაცემთა ბაზებთან მუშაობის ტესტირება
    • ინფორმაციის ამოღება მბ-დან
    • ინფორმაციის შეტანა მბ-ში
    • ინფორმაციის განახლება მბ-ში
    • ინფორმაციის წაშლა მბ-ში
  • Sanctum პაკეტი
    • მომხმარებელთა რეგისტრაცია
    • მომხმარებელთა ავტორიზაცია
  • Postman
    • API-ს გატესტვა Postman-ის მეშვეობით

პროექტის ინსტალაცია და გამართვა

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;

// აქ დავწერთ მარშრუტებს ^_^
            

ჩვენი პირველი API ტესტი

ჯერ ტესტი, მერე კოდი :)) მაშ ასე :
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()));
    }
}
            

ამჯერად სისტემა უფრო მეტ ინფორმაციას გამოგვიტანს კულისებს მიღმა მიმდინარე პროცესებზე :



რაც იმას ნიშნავს, რომ მოთხოვნა გავაგზავნეთ არარსებულ მისამართზე. აღვწეროთ მარშრუტი, routes/api.php :
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' => []]); // ამ ეტაპზე დავაბრუნოთ ცარიელი მასივი
    }
}
            
ლურჯად მონიშნული ქვე-საქაღალდის დასახელება, რა თქმა უნდა, პირობითია და შეგვიძლია საერთოდ არც გავაკეთოთ ახალი საქაღალდე, მაგრამ კარგი პრაქტიკაა API-ზე მუშაობისას ცალკე გავიტანოთ ხოლმე საჭირო კონტროლერები.
TodoListTest.php :
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, ვიხილავთ ამდაგვარ შედეგს :



ახლა კი სულ ოდნავ შევალამაზოთ ჩვენი კოდი. შევთანხმდით, რომ სასურველია თუ სრულ შეტყობინებებს ვნახავთ ხოლმე შეცდომების შესახებ, მაგრამ საკმაოდ არაკომფორტული იქნება withoutExceptionHandling მეთოდის გამოძახება სათითაოდ ყველა ტესტში, ამიტომ ეს გავაკეთოთ tests/TestCase.php ფაილში აღწერილ კლასში შემდეგნაირად :
namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    public function setUp(): void
    {
        parent::setUp();
        $this->withoutExceptionHandling();
    }
}
            

ინფორმაციის ამოღება მბ-დან

მბ-დან ინფორმაციის ამოსაღებად მივმართოთ მოდელს, TodoListController :
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();
    });
}
            

მოდელმწარმოებელი

იმაზე, თუ რა არის მოდელმწარმოებელი, უკვე ვისაუბრეთ 26-ე თავში, ამიტომ შევუდგეთ უშუალოდ მის გამოყენებას, 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();

        // ქმედება
        $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']);
    }
}
            

კონკრეტული ჩანაწერის ამოღება ბაზიდან

შევქმნათ ფუნქციონალი, რომელიც იდენტიფიკატორის მეშვეობით გამოგვიტანს კონკრეტულ ჩანაწერს მონაცემთა ბაზის ცხრილიდან. TodoListTest.php ფაილში ჩავამატოთ ახალი ტესტი 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));

    // მტკიცება
    $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);
}
            

ტესის დახვეწა

კოდის დასახვეწად, ჩვენს მარშრუტს მივამაგროთ მოდელი, 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');
            
კონტროლერის 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);
    }
}
            
როგორც ვხედავთ, მომზადების ფაზის აღწერა აღარ გვჭირდება სათითაოდ ყველა ტესტში.

ახალი ჩანაწერის ჩამატება ბაზაში

ტრადიციულად დავიწყოთ ტესტით, ჩავამატოთ ახალი ტესტი TodoListTest.php ფაილში :
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.

ვალიდაციის ტესი

ახალი ჩანაწერის დამატებისას დარწმუნებულები უნდა ვიყოთ, რომ ყველა საჭირო ინფორმაცია ჩაიდება მოთხოვნაში. ჩვენს შემთხვევაში ახალი სიის დამატებისას აუცილებლად უნდა გაიგზავნოს სიის დასახელება (name ველი). დავწეროთ ტესტი, რომელიც გადაამოწმებს სწორად გვაქვს თუ არა გამართული ვალიდაციის სისტემა. ჩავამატოთ ახალი ტესტი TodoListTest.php ფაილში :
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());
}
            

ჩანაწერის წაშლა და განახლება

ჩანაწერის წაშლა

ჩავამატოთ ჩანაწერის წაშლის ტესტი TodoListTest.php ფაილში :
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);
}
            
ამჯერად ტესტი წარმატებით დასრულდება.

ჩანაწერის განახლება

ჩავამატოთ ჩანაწერის განახლების ტესტი TodoListTest.php ფაილში :
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);
}            
            
ამჯერად ტესტი წარმატებით დასრულდება.

ჩანაწერის განახლების ვალიდაციის ტესტს ექნება ზუსტად იგივე შინაარსი რაც დამატებისას ჰქონდა.

კოდის დახვეწა

apiResource

routes/api.php ფაილს თუ დავაკვირდებით, შევამჩნევთ, რომ მხოლოდ CRUD ოპერაციების მარშრუტები გვაქვს აღწერილი (CRUD-ის შესახებ ინფორაცია იხილეთ მე-9-ე თავში), ანუ ვქმნით, ვარედაქტირებთ, ვკითხულობთ და ვშლით კონკრეტულ რესურსებს (todo-list). ასეთ შემთხვევაში, ერთმანეთის მსგავსი სხვადასხვა მარშრუტების წერის ნაცვლად, შეგვიძლია გამოვიყენოთ apiResource, რომელსაც პირველ პარამეტრად უნდა გადავცეთ, რესურსის დასახელება, მეორე პარამეტრად კი სასურველი კონტროლერი :
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);
    }
}       
            

ვალიდაციის მექანიზმი

კონტროლერის store და update მეთოდებს თუ დავაკვირდებით, შევამჩნევთ, რომ ზუსტად ერთნაირი ვალიდაცია გვაქვს ორივე მათგანში. ამის თავიდან ასაცილებლად გამოვიყენოთ ლარაველში ჩადგმული FormRequest კლასი, რომელიც საშუალებას გვაძლევს, მის მემკვიდრე კლასებში მოვახდინოთ, კონკრეტული ამოცანის შესაბამისი ვალიდაციის წესების ინკაფსულაცია. ჩვენს შემთხვევაში გვჭირდება დავალებათა სიის (todo-list) შექმნისა და განახლების ვალიდაციები. გავუშვათ შემდეგი ბრძანება :
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 კლასის ინექცია, ამ მეთოდებში ვალიდაციის ინსტრუქციების აღწერა აღარ გვიწევს. ტესტიც კვლავინდებულად წარმატებულად დასრულდება.

POSTMAN

კიბატონო, ტესტებში მშვენივრად მიგვდის საქმე ^_^ მაგრამ მოდით POSTMAN-შიც შევიხედოთ :)) გადავამოწმოთ როგორ მუშაობს ჩვენი პროექტი ტესტებს მიღმა სამყაროში :)) ამისათვის, პირველ რიგში, .env ფაილში განვსაზღვროთ რეალურ მონაცემთა ბაზასთან სამუშაო პარამეტრები (ეს უკვე იმაზეა დამოკიდებული თუ როგორ გაქვთ მოწყობილი ლოკალური სერვერი) :
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
            

ჩანაწერის შექმნის წერტილი

ჩანაწერთა სიის წერტილი

კონკრეტული ჩანაწერის წერტილი

ჩანაწერის განახლების წერტილი


ჩანაწერის წაშლის წერტილი


ჩანაწერის წაშლის წერტილში მოვიქცეთ ქვემოთ მოყვანილი ფოტოს მიხედვით, ანუ Content-Type და Accept პარამეტრების მნიშვნელობებად განვსაზღვროთ application/json და შევეცადოთ უკვე წაშლილი ჩანაწერის წაშლას, ვიხილავთ შედეგს, რომელიც ამავე ფოტოზე ჩანს :


.env ფაილში შევიტანოთ შემდეგი ცვლილება :
APP_DEBUG=false
            
ამჯერად შედარებით ლაკონურ და მოკლე შეტყობინებას ვიხილავთ.

დავალებები (todo list task)

დავალებების ჩამონათვალი

გავუშვათ ბრძანება :
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);
    }
}
            
ამჯერად ტესტი წარმატებით დასრულდება.

დავალების შექმნა

TaskTest კლასში ჩავამატოთ ახალი ტესტი :
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());
}
            

დავალების წაშლა

TaskTest კლასში ჩავამატოთ ახალი ტესტი :
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);
            

სიისა და დავალების მარშრუტთა დაკავშირება

სიებსა და დავალებებს შორის განვსაზღვროთ ასეთი ურთიერთკავშირი : ნებისმიერ სიას უნდა გააჩნდეს ერთი ან რამდენიმე დავალება, ნებისმიერ დავალებას კი უნდა ჰყავდეს ერთი მშობელი სია. ამ კავშირიდან გამომდინარე, ცვლილებები მოგვიწევს, როგორც მარშრუტებში, ასევე ტესტებშიც. დავიწყოთ მარშრუტებით. როგორც ვიცით, დავალებებში გვაქვს ხუთი წერტილი :

  • ჩამონათვალის წერტილი (index) - საჭიროა სიის დაკონკრეტება რათა ვიცოდეთ, რომელი სიის დავალებების ჩამონათვალი გვინდა.
  • დამატების წერტილი (store) - საჭიროა სიის დაკონკრეტება რათა ვიცოდეთ, რომელ სიაში ვამატებთ დავალებას
  • ნახვის წერტილი (show) - აქ არ არის საჭირო სიის დაკონკრეტება, გვჭირდება მხოლოდ დავალების იდენტიფიკატორი (ID).
  • რედაქტირების წერტილი (update) - არც აქ არის საჭირო სიის დაკონკრეტება.
  • წაშლის წერტილი (destroy) - არც აქ არის საჭირო სიის დაკონკრეტება.

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
ამ ყველაფრის შემდეგ TaskTest კლასს უნდა მივცეთ ასეთი სახე :
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]);
    }
}
            

დავალების განახლება

TaskTest კლასში ჩავამატოთ ახალი ტესტი :
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;
}
            

სიისა და დავალების რელაციური დაკავშირება

თუ ვამბობთ, რომ ნებისმიერ დავალებას უნდა ჰყავდეს ერთი მშობელი სია, ეს იმას ნიშნავს, რომ დავალებების ცხრილში უნდა გვქონდეს რაიმე ველი, რომელიც ამ კავშირს განსაზღვრავს. test_store_task_for_todo_list ტესტში შევიტანოთ ასეთი ცვლილება:
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()
        ];
    }
}    
            

hasMany და belongsTo კავშირები

როგორც ადრე აღვნიშნეთ სიას შეიძლება გააჩნდეს მრავალი დავალება, დავალებას კი ერთი მშობელი სია, აღვწეროთ ეს კავშირები და ტრადიციულად დავიწყოთ ტესტებით. რადგან ამ კონკრეტულ შემთხვევაში მოხოლდ ცალკეულ ფუნქციებს გავტესტავთ, გამოვიყენოთ ფრაგმენტული ტესტები :
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;
    }
}
            

POSTMAN

დავუბრუნდეთ POSTMAN-ს :)) გადავამოწმოთ კონკრეტული სიის დავალებათა გამოტანის წერტილი :



.env ფაილში გავააქტიუროთ შეცდომათა გამოტანის რეჟიმი :
APP_DEBUG=true
            
ამის შემდეგ გავუშვათ მიგრაციების შესრულების ბრძანება : php artisan migrate ამის შემდეგ, იგივე წერტილზე მიკითხვისას, დაგვიბრუნდება ცარიელი კოლექცია. შევქმნათ რაიმე დავალება :



GET ტიპის localhost:8000/api/todo-list/2/task წერტილი ამჯერად უკვე დაგვიბრუნებს id=2 სიის დავალებათა ჩამონათვალს :



ახლა შევამოწმოთ კონკრეტული დავალების წაშლის წერტილი :



შევამოწმოთ დავალების განახლების წერტილიც :



როგორც ვხედავთ ჩანაწეტი არ განახლდა, მოვიქცეთ ასე :



დავალების სტატუსი

ამ თავში ვიმუშავოთ დავალების სტატუსის ცვლილებაზე. პირველ რიგში, მიგრაციის ფაილში ჩავამატოთ ახალი ველი, 2022_11_24_222616_create_tasks_table :
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']);
    }
}
            

onDelete cascade

წარმოვიდგინოთ, რომ გვაქვს დავალებათა სია, რომელიც მოიცავს 10 დავალებას. როდესაც ამ სიას წავშლით, ლოგიკური იქნება თუ მისი შვილობილი დავალებებიც წაიშლება დავალებათა ცხრილიდან. ფრაგმენტულ ტესტ TodoListTest-ში ჩავამატოთ ახალი ფუნქცია :
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();
    });
}
            

მომხმარებელთა აუტენტიფიკაცია

რეგისტრაცია

წარმოვიდგინოთ, რომ დავალებათა კონკრეტული სიები ეკუთვნის კონკრეტულ, მომხმარებლებს. ასეთ შემთხვევაში თითოეულ მომხმარებელს უნდა შეეძლოს მხოლოდ თავისი სიების ნახვა. ამისათვის გამოიყენება აუტენტიფიკაციის სისტემა. ამ სისტემის ტესტები მოვათავსოთ tests/Feature/Auth საქაღალდეში :
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'],
        ];
    }
}
            
ამ ეტაპზე ტესტი წარმატებით გაეშვება.

Sanctum პაკეტი

Laravel Sanctum პაკეტი გამოიყენება REST API-ში შემთხვევით ტექსტებზე ანუ ე.წ თოქენებზე დაფუძნებული ავტორიზაციის სისტემების ჩასაშენებლად. თოქენები ინახება მონაცემთა ბაზაში, თოქენი უნდა გაიწეროს თითოეული HTTP მოთხოვნის Authorization სათაურში (HEADER). Sanctum-ის ინსტალაცია ავტომატურად ხდება laravel-ის ინსტალაციისას, composer.json :



რაც იმას ნიშნავს, რომ ამ პაკეტის გამოყენებისათვის საჭირო მოსამზადებელი სამუშაოები უკვე ჩატარებულია ჩვენს პროექტში.

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
            

Sanctum შუამავალი

იმისათვის რათა, კონკრეტული მარშრუტები მოექცეს Sanctum-ის ზედამხედველობის ქვეშ, უნდა გამოვიყენოთ auth:sanctum შუამავალი, 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::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');
            
გავუშვათ ტესტები.



არ ინერვიულოთ, ეს Sanctum-მა ქნა :))) ყველაფერს ეშველება, თანაც მალე ^_^



tests/TestCase.php ფაილში შევიტანოთ შემდეგი ცვლილებები :
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();
    }
}
            

რეგისტრაციისა და ავტორიზაციის წერტილები POSTMAN-ში







დავალებათა მიკუთვნება მომხმარებლებზე

tests/Feature/TodoListTest კლასის test_todo_list_index მეთოდს თუ დავაკვირდებით ვნახავთ, რომ ბაზაში არსებული ყველა სიის ამოღებას ვახდენთ, რაც არასწორია, რადგან ამ მომენტისათვის უკვე გვყავს ავტორიზებული მომხმარებლები და სწორი იქნება თუ ისინი მხოლოდ თავიანთ სიებს ნახავენ. ამისათვის, პირველ რიგში, მომხმარებლები უნდა დავაკავშიროთ სიებთან. მივმართოთ 2022_11_05_074223_create_todo_lists_table მიგრაციის ფაილს :
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
            


პრაქტიკა



1. პროლოგი
ცნობარის პრაქტიკულ ნაწილში შევქმნით ორენოვან ბლოგს. ადმინისტრატორს შესაძლებლობა ექნება დაამატოს, წაშალოს, დაარედაქტიროს და გამოაქვეყნოს სიახლეები, ასევე დაადასტუროს და გამოაქვეყნოს, მომხმარებლების მიერ კონკრეტულ სიახლეებზე დატოვებული კომენტარტები. რეგისტრირებულ მომხმარებლებს შეეძლებათ დატოვონ კომენტარები კონკრეტულ სიახლეებზე, დაარედაქტირონ პირადი მონაცემები და ა.შ.

ასევე განვიხილავთ ისეთ სისტემებსა და ინსტრუმენტებს, რომლებთანაც აქამდე არ გვქონია შეხება. მაგალითად მუშაობა მოგვიწევს ფაილებთან, ქეშირებასთან, ჰეშირებასთან, ასევე ვისწავლით Ajax-თან მუშაობას, დავაინსტალირებთ სხვადასხვა პაკეტებს და ა.შ ...

მომავალი პროექტის ადმინისტრატორის მხარისთვის გამოვიყენოთ Bootstrap ფრეიმვორკის მარტივი შაბლონი, რომლის გადმოწერაც შეგიძლიათ ამ ბმულიდან.

მომხმარებლის მხარისათვის კი გამოვიყენოთ ეს შაბლონი.

მზა პროექტის გადმოსაწერად მიჰყევით ამ ბმულს.

2. პროექტის ინსტალაცია და მონაცემთა ბაზის დაგეგმარება
პირველ რიგში დავაინსტალიროთ ახალი პროექტი :
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 ასეთ შემთხვევაში, ზემოთხსენებული პაკეტის ინსტალაციის შემდეგ, ბრაუზერში ვიხილავთ ამდაგვარ პანელს :



მონაცემთა ბაზა

მომხმარებლების ცხრილი

როგორც ვიცით, Laravel-ის ინსტალაციის შემდეგ ავტომატურად იქმნება მიგრაციის რამდენიმე ფაილი. ერთ-ერთი მათგანია მომხმარებლებთან სამუშაო ცხრილის მიგრაციის ფაილი, რომელსაც აქვს შემდეგი სტრუქტურა :
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();
    });
}
                
ასე რომ, მომხმარებლების ცხრილის მიგრაციის ფაილი უკვე გვაქვს.

ადმინისტრატორების ცხრილი

საკმაოდ ხშირი პრაქტიკაა, როდესაც ადმინისტრატორებისთვისაც users ცხრილს იყენებენ და შემდეგ როლის მიხედვით ანსხვავებენ ხოლმე სტანდარტულ მომხმარებლებასა და ადმინსტრატორებს, გააჩნია პროექტის შინაარს, მასშტაბურობას, მიზნებს და ა.შ. ამჯერად ადმინისტრატორებისათვის შევქმნათ სრულიად დამოუკიდებელი მოდული ცხრილითურთ. შევქმნათ მიგრაციის ფაილი :
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();
    });
}
                

მონაცემთა ბაზის კონფიგურაციული პარამეტრები

ახლა .env ფაილში განვსაზღვროთ მონაცემთა ბაზის კონფიგურაციული პარამეტრები :
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

არსებობოს მშვენიერი ხელსაწყო dbdiagram.io, რომელიც გვიმარტივებს მონაცემთა ბაზის დაგეგმარების პროცესს და ასევე გვაძლევს საშუალებას თვალი ვადევნოთ ბაზაში შემავალი ცხრილების ურთიერთდამოკიდებულებებს და ა.შ. შეგვიძლია უკვე არსებული ბაზის dbdiagram.io-ში დაიმპორტება და პირიქითაც - dbdiagram.io-ზე შექმნილი ბაზის დაექსპორტებაც.

ჩვენი პროექტის ბაზას 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]
}
                




3. ადმინისტრატორების მოდული

Admin მოდელი

შევქმნათ ადმინისტრატორების ცხრილთან და მოდულთან სამუშაო მოდელი - Admin :
php artisan make:model Admin
                
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Admin extends Model 
{
    //    
}
                

საწყისი ინფორმაციის შეტანა ადმინისტრატორების ცხრილში

ადმინსტრატორების ცხრილში საწყისი ინფორმაციის შესატანად გამოვიყენოთ ე.წ 'სიდერების' (seeders) მექანიზმი :
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
                

BaseController

ადმინისტრატორის მხარის კონტროლერებისათვის შევქმნათ app/Http/Controllers/Admin საქაღალდე, მასში კი BaseController კონტროლერი, რომლის მემკვიდრეც იქნება ადმინისტრატორის მხარის ყველა კონტროლერი :
php artisan make:controller Admin/BaseController
                
namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;

class BaseController extends Controller
{
    //
}
                

AdminsController

ახლა შევქმნათ ადმინისტრატორების მოდულთან სამუშო კონტროლერი - AdminsController, რომელიც, როგორც ვთქვით, იქნება Admin/BaseController კონტროლერის მემკვიდრე :
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)
    {
        //
    }
}
                
4. ადმინისტრატორის ავტორიზაცია, წარმოდგენის ფაილების დაგეგმარება
პირველ რიგში განვსაზღვროთ ადმინისტრატორის ავტორიზაციის მარშრუტი, routes/web.php :
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');        
    }    
}
                

წარმოდგენის ფაილები

ადმინისტრატორის მხარის წარმოდგენის ფაილებისათვის შევქმნათ საქაღალდე resources/views/admin, მასში კი ფაილი - login.blade.php, რომელშიც ჩავაკოპირებთ გადმოწერილი Bootstrap შაბლონის login.html ფაილის მაქსიმალურად გამარტივებულ კოდს :
<!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 მისამართზე, ვიხილავთ ავტორიზაციის ფორმას.

ავტორიზაცია

ავტორიზაციისათვის განვსაზღვროთ post ტიპის AdminLogin მარშრუტი, 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');
                
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.

ადმინისტრატორის განყოფილების მთავარი გვერდი

შევქმნათ მარშრუტი ადმინისტრატორის განყოფილების მთავარი გვერდისათვის. 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::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');

});
                
როგორც ვხედავთ, ამ ეტაპზე სისტემიდან გამოსვლისათვის შევქმენით get ტიპის მარშრუტი. მომავალში ამ მარშრუტს ჩავანაცვლებთ post ტიპის მარშრუტით და ასევე ვისაუბრებთ ამ ცვლილების მიზეზებზეც.
აღვწეროთ logout მეთოდი LoginController კონტროლერში :
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 ფაილს, ანუ ადმინისტრატორის განყოფილების მთავარ გვერდზე მოვხვდებით ისე, რომ ავტორიზაცია არ გვექნება გავლილი ...



შუამავალი Admin

ამ პრობლემის მოგვარებაში დაგვეხმარება შუამავალი, რომელიც გადაამოწმებს, შეანახულია თუ არა სესიაში, ავტორიზებული ადმინისტრატორის შესაბამისი მოდელი, თუ კი - მაშინ შეგვიშვებს ადმინისტრატორის განყოფილებაში, წინააღმდეგ შემთხვევაში გადაგვამისამართებს ავტორიზაციის გვერდზე.
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');
    
});
                
5. ადმინისტრატორის მხარის მთავარი შაბლონი
შევქმნათ ფაილი resources/views/admin/layout.blade.php და შევიტანოთ მასში შემდეგი კოდი :
<!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
                
წითელი ფერით მონიშნულია ის ადგილები სადაც შაბლონიზატორის სინტაქსით დაგვჭირდა ჩარევა, ვფიქრობ ყველაფერი გასაგებია და ახალი არაფერია ამ სინტაქსში, შესაბამისად დაკონკრეტებაზე აღარ დავკარგოთ დრო.

თუ ახლა შევალთ ადმინისტრატორის პანელის მთავარ გვერდზე, ვიხილავთ ამდაგვარ სურათს :





შევნიშნოთ, რომ ამჯერად უკვე ავტორიზაციის გავლა მოგვიწევს ამ გვერდზე მოსახვედრად (ელ_ფოსტა : admin@admin.com, პაროლი : admin123).
6. ადმინისტრატორების დამატება, რედაქტირება, წაშლა და ჩამონათვალის გვერდი

მარშრუტები

პირველ რიგში შევქმნათ მარშრუტები ადმინისტრატორების მოდულისათვის :
use App\Http\Controllers\Admin\AdminsController;

Route::resource('admins', AdminsController::class);
                
როგორც ვიცით, Route ფასადის resource მეთოდი დააგენერირებს მარშრუტებს, შემდეგი მახასიათებლების:
  • ტიპი
  • ბმული
  • სახელი
  • კონტროლერის მეთოდი
შემდეგი მნიშვნელობებით :



რესურსის დამატების შაბლონი

შევქმნათ resources/views/admin/admins საქაღალდე, მასში კი create.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">
        <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
                

რესურსის დამატების მეთოდები კონტროლერში

გადავინაცვლოთ AdminsController-ში და შევიტანოთ შესაბამისი ცვლილებები :
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 მისამართზე, ვიხილავთ ადმინისტრატორის დამატების ფორმას.

store() მეთოდი

განვიხილოთ store მეთოდი. ვალიდაციის მონაკვეთში ჩვენთვის ახალი ერთადერთი ჩანაწერია : '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

                

ჩანაწერის წაშლა

როგორც ვიცით Route ფასადის resource მეთოდი ჩანაწერების წაშლისათვის აგენერირებს DELETE ტიპის მარშრუტს. HTML ფორმები კი მხარს არ უჭერენ PUT, PATCH და DELETE მეთოდებს. ასეთ შემთხვევებში უნდა შევქმნათ ფორმები, რომლებსაც ექნებათ hidden ტიპის _method სახელიანი და ასევე csrf ველები :
<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                
                

update() მეთოდი

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>

...
                
7. საკონტაქტო ინფორმაციის მოდული
როგორც გვახსოვს, საკონტაქტო ინფორმაციისათვის შევქმენით contacts ცხრილი. ბუნებრივია ეს ინფორმაცია არც ისე ხშირად შეიცვლება და არც ჩანაწერების დამატებისა და ჩამონათვალის გვერდები გვექნება, რადგან ცხრილში სულ ერთ ჩანაწერს შევიტანთ და შემდეგ დავარედაქტირებთ ხოლმე მას.

საწყისი ინფორმაციის შეტანა საკონტაქტო ინფორმაციის ცხრილში

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.

კონტროლერი

როგორც ადრე აღვნიშნეთ, კარგი პრაქტიკაა მბ-სთან სამუშო ფუნქციონალის მოდელებში აღწერა, მაგრამ დროის ეკონომიის მიზნით და ასევე ამ მოდულის სიმარტივის გათვალისწინებით, ამჯერად კონტროლერშივე მივმართოთ DB ფასადს :
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();  
    }
}                
                

ინფორმაციის რედაქტირების გვერდი

შევქმნათ საქაღალდე resources/views/admin/contact, მასში კი ფაილი 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">
            
            <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 მეთოდს :
Cache::forget('key');
                

ინფორმაციის არსებობა/არარსებობის გადამოწმება ქეშში

იმის გასაგებად, არსებობს თუ არა ქეშში კონკრეტული გასაღების შესაბამისი ინფორმაცია, უნდა მივმართოთ Cache ფასადის has მეთოდს :
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>

...
                
8. მრავალენოვანი სისტემის გამართვა
მრავალენოვანი სისტემის გასამართად გამოვიყენოთ ეს პაკეტი :
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,
                

ძირითადი ენის განსაზღვრა

დავუშვათ გვინდა, რომ აპლიკაციის ძირითადი ენა იყოს ქართული, ამისათვის config/app.php ფაილში აღწერილი locale პარამეტრის მნიშვნელობად უნდა მივუთითოთ ქართული ენის ინდექსი :
...

'locale' => 'ka',

...
                

შუამავლები

გადამისამართებები დაგვჭირდება სხვა შემთხვევებშიც, მაგალითად თუ მომხმარებელს უნდა შესვლა http://127.0.0.1:8000/test მისამართზე, სისტემამ ავტომატურად უნდა დააგენერიროს ძირითადი ენის შესაბამისი ბმული (http://127.0.0.1:8000/ka/test), ენების გათვალისწინება დაგვჭირდება სესიებთან მუშაობისასაც და ა.შ, ამიტომ app/Http/Kernel.php ფაილში აღწერილ $routeMiddleware თვისებაში უნდა ჩავამატოთ შემდეგი შუამავლები :
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

];
                

მარშრუტები

ამ ეტაპზე მრავალენოვანი სისტემა გამოვიყენოთ მხოლოდ მომხმარებლის მხარისათვის, 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;


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
                
9. სიახლეების მოდული

Article მოდელი

შევქმნათ სიახლეების ძირითად ცხრილთან (articles) და მოდულთან სამუშაო მოდელი - Article :
php artisan make:model Article
                
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    // 
}
                

ArticlesTranslate მოდელი

როგორც ვიცით სიახლეებთან სამუშოდ გვაქვს ორი - ძირითადი (articles) და სათარგმნი (articles_translates) ცხრილები. შევქმნათ სიხლეების თარგმანებთან სამუშო მოდელი :
php artisan make:model ArticlesTranslate
                
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ArticlesTranslate extends Model
{
    //
}
                

ურთიერთკავშირი ცხრილებს შორის

გამომდინარე იქიდან, რომ ჩვენი საიტი ორენოვანია, სიახლიეების ძირითადი ძირითადი ცხრილის თითოეულ ჩანაწერს, სათარგმნ ცხრილში 'ეყოლება' ორი 'შვილობილი' ჩანაწერი. აღვწეროთ ეს დამოკიდებულება Article და ArticlesTranslate მოდელებში :
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);
    }
}
                

ArticlesController

ახლა შევქმნათ სიახლეების მოდულთან სამუშო კონტროლერი - ArticlesController, რომელიც, ასევე Admin/BaseController კონტროლერის მემკვიდრე იქნება :
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)
    {
        //
    }
}
                

ხელმისაწვდომი ენების გაზიარება წარმოდგენის ფაილებში

სისტემა არის ორენოვანი, ეს იმას ნიშნავს, რომ სიახლეების დამატებისა და რედაქტირების წარმოდგენის ფაილებში, ისეთი ველებისათვის, როგორებიცაა - სათაური, აღწერა და სრული ტექსტი, დაგვჭირდება ცალ-ცალკე ველები. ბუნებრივია ამ ფაილებს საიდანღაც უნდა მივაწოდოთ ინფორმაცია ხელმისაწვდომი ენების შესახებ. ეს გავაკეთოთ BaseController-ში :
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() მეთოდი კი ხელმისაწვდომ ენებს გვიბრუნებს შემდეგი სახით :



$locales ცვლადის მეშვეობით ახლა უკვე შეგვიძლია წარმოდგენის ფაილებში ვიმუშავოთ ენებთან.

მარშრუტები

შევქმნათ მარშრუტები სიახლეების მოდულისათვის :
use App\Http\Controllers\Admin\ArticlesController;

Route::resource('articles', ArticlesController::class);
                

რესურსის დამატების შაბლონი

შევქმნათ resources/views/admin/articles საქაღალდე, მასში კი create.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">
        <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
                
ამ ფორმის მეშვეობით გაიგზავნება შემდეგი შინაარსის მოთხოვნა :



რესურსის დამატების მეთოდები კონტროლერში

გადავინაცვლოთ 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()
    {
        //
    }

    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 მისამართზე, ვიხილავთ სიახლის დამატების ფორმას.

store() მეთოდი

განვიხილოთ store მეთოდი.

როგორც ვიცით, 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
                

ჩანაწერის წაშლა

Route ფასადის resource მეთოდის მიერ დაგენერირებული delete ტიპის მარშრუტის შესახებ უკვე ვისაუბრეთ ადმინისტრატორების მოდულში, ასევე მოვიყვანეთ და ავხსენით ამ მეთოდით ჩანაწერების წაშლის მექანიზმი, აქაც ზუსტად იგივე სიტუაციაა, ამიტომ აღარ დავკონკრეტდებით.

ჩანაწერის წაშლის მეთოდი კი 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 ციკლში გატარებული ხელმისაწვდომი ენებიდან, ციკლის მიმინარე იტერაციის შესაბამისი ინფორმაციის ამოღება ზემოთნახსენები ორელემენტიანი კოლექციიდან.

კონტროლერის update() მეთოდი

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>

...
                
10. მომხმარებლის მხარის ძირითადი კონტროლერი
შევქმნათ საქაღალდე app/Http/Controllers/Front, მასში კი მომხმარებლის მხარის ძირითადი კონტროლერი IndexController:
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);

});    
                
თუ ახლა შევალთ პროექტის მთავარ გვერდზე, ვიხილავთ ტექსტს - 'მთავარი გვერდი'.
11. მომხმარებლის მხარის შაბლონების გამართვა
შევქმნათ საქაღალდე resources/views/front, მასში კი მომხმარებლის მხარის ძირითადი შაბლონი layout.blade.php, რომელშიც ამ ეტაპზე განვსაზღვრავთ ერთადერთ სექციას სახელად - content:
@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
                

სათარგმნი ფაილები

პროექტის ინსტალაციის შემდეგ lang საქაღალდეში შეიქმნებოდა ერთადერთი საქაღალდე - en ინგლისური ენისათვის. დავაკოპიროთ იგი და ასლს სახელად დავარქვათ ka. ახლა კი ორივე მათგანში შევქმნათ ფაილი menu.php შემდეგი კოდებით :
<?php

return [
    'index' => 'Home',
    'contact' => 'Contact',
];
                
<?php

return [
    'index' => 'მთავარი',
    'contact' => 'კონტაქტი',
];
                
12. სიახლეების გამოტანა მთავარ გვერდზე
კონტროლერის index მეთოდში მოვახდინოთ სიახლეების ამოღება ბაზიდან, შემდეგ კი ეს ინფორმაცია გადავცეთ წარმოდგენის ფაილს :
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) შექმნა უკვე ვიცით და ამიტომ აღარ დავკონკრეტდებით.
13. კონკრეტული სიახლის გვერდი
პირველ რიგში შევქმნათ კონტროლერის მეთოდი :
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
                
14. მომხმარებლების რეგისტრაცია და აუტენტიფიკაცია
მომხმარებელთა რეგისტრაციისა და აუტენტიფიკაციისათვის გამოვიყენოთ, ჩვენთვის უკვე კარგად ნაცნობი - Laravel Breeze :
composer require laravel/breeze --dev
                
ამის შემდეგ გავუშვათ ეს ბრძანება :
php artisan breeze:install
                
როგორც ვიცით, ინსტალაციის შემდეგ იქმნება routes/auth.php ფაილი და ასევე ხდება ამ ფაილის routes/web.php ფაილში გამოძახება :
php require __DIR__.'/auth.php';
                
თუმცა !
პაკეტის ინსტალაციისას იშლება უკვე არსებული routes/web.php ფაილი და იქმნება ახალი, შესაბამისად - იკარგება უკვე შექმნილი მარშრუტებიც !!! რაც, ჩემი აზრით, არც ისე სასიამოვნო სიურპრიზია და ეს მომენტი აუცილებლად უნდა გავითვალისწინოთ ხოლმე ამ პაკეტის გამოყენებისას. თუ ვქმნით პროექტს, რომელშიც გვაქვს რეგისტრაცია/აუტენტიფიკაციის სისტემა, უმჯობესია, რომ ამ პაკეტის ინსტალაციით დავიწყოთ ხოლმე მუშაობა, ხოლო თუ უკვე არსებულ პროექტში გვიწევს ამ სისტემის ჩაშენება მაშინ პაკეტის ინსტალაციამდე აუცილებლად უნდა შევინახოთ ძველი მარშრუტები.
ეს კი 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::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';
                

რეგისტრაცია

როგორც ვიცით, Laravel Breeze-ის ინსტალაიის შემდეგ, აუტენტიფიკაცია/რეგისტრაციასთან დაკავშირებული ფაილები შეიქმნებოდა შემდეგ საქაღალდეებში : კონტროლერები - App/Http/Controllers/Auth საქაღალდეში, წარმოდგენის შაბლონები - resources/views/auth საქაღალდეში. თუ ახლა გავხსნით http://127.0.0.1:8000/register ბმულს, ვიხილავთ რეგისტრაციის გაუსტილავ ფორმას. გამოვასწოროთ ეს ხარვეზი, resources/views/auth/register.blade.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';

...
                

UserController

მომხმარებლისათვის საჭირო ფუნქციონალთან სამუშაოდ შევქმნათ კონტროლერი :
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();        
    }
}                
                
ამ კოდში ახალი და განსაკუთრებული არაფერია და ამიტომ დაკონკრეტებაზე აღარ დავკარგავ დროს.

რეგისტრაცია/ავტორიზაციისა და სისტემიდან გასვლის ბმულები

ჩვენი პროექტის ძირითად შაბლონში ჩავამატოდ რეგისტრაცია/ავტორიზაციისა და სისტემიდან გასვლის ბმულები, resources/views/front/layout.blade.php ფაილში, კონტაქტის გვერდის ბმულსა და ენების გადამრთველს შორის ჩავამატოთ შემდეგი კოდი :
@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