Софтуерно Инженерство
Loading...
+ Нов въпрос
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки

PHP Web Development - Отговори на въпроси от sli.do

Здравейте,

Виждам, че има няколко въпроса във sli.do, бих ги отговорил и на лекцията, но някои от тях може да изисквам дискусия, а не съм сигурен колко от задалите ги са присъствени, за това ще дам отговори и във форума, в случай че някой иска да се включи

Малко се връщам назад ... може ли да обясниш както точно прави ::class и как можем да го заместим? Пр: \Core\MVC\MVCContextInterface::class

 

Псевдоконстантата class връща името на класа (пълното му име, включително и namespace) като стринг. Може да се замести с ръчно писане на стринга, но рискуваме при промяна на името на класа, да не променим стринга, докато така извиквайки го директно от класа трудно може да стане същия проблем. В тази статия има примерче и как се държи PHPStorm в двата сценария.

Как да добавим къстъм клас и action във fromBulder-a пробвах с $builder->setAttributes() и нестана

 

За да се сложи на самия <form> таг action се ползва $builder->setAction(); а за да се сложи class, id или някакъв друг атрибут става през HTML-a (Twig-a):  {{ form_start(form, {attr: {'class': 'my-form-class'}}) }}

Не разбрах защо се прави контролер, който не наследява предложения от Symfony. Бихте ли обяснили отново!

 

Истината е, че в 99% от случаите няма добра причина да се прави. Когато контролерът се изнесе като Service, в голяма част от случаите не може да наследява предложения от Symfony контролер. Понякога изнасяме контролер като Service когато се нуждае от това да му бъдат добавени (inject-нати) зависимости (dependency-та). Едното идва на цената на другото, аз лично предлагам да си наследявате стандартния Symfony контролер и когато дойде нуждата да се използва dependency injection-а на редица други обекти, то според ситуацията ще разберете трябва ли да спре да наследява държавния контролер или не.

Може ли в резюме да кажете отново как се завърта един цикъл от работата на един контролер в Symfony? ....  Как и къде се извиква Контролера? Как се вземат данните за модела и как се извиква Вю-то?

 

На този въпрос ще отговоря и на лекция нагледно, но с няколко думи тук - първо един полезен линк - https://symfony.com/doc/current/controller.html. Symfony сканира всички класове в Bundle-ите и търси за анотации @Route и евентуално @Method, над методите им. Ако присъстват такива, то за Symfony това са годни за извикване методи при определени събития. Като последните се контролират от това какво пише в @Route и @Method. В първото е URI шаблонът в адрес бара (пр. /users/register), а второто е HTTP метод (пр. POST, ако анотацията не е написана по подразбиране е GET). Та, ако настъпи събитието /uses/register и метод GET се извиква съответният метод, който има тези анотации. Той приема потребителските данни през URL-а, тъй като е GET. Т.е. ако шаблонът беше /users/{id} (т.е. /users/ и нещо след него), то методът щеше да приема като аргумент променливата $id. Тя щеше да се напълни с данните равни на тези в адрес бара, например ако потребителят беше извикал /users/4, променливата $id щеше да има стойност 4. Вю-то от своя страна го управлява контролера, той в края на изпълнението си трябва да върне някакъв HTTP Response, например redirect или View. Последното се рисува на екрана чрез функцията render().

Ако данните, които трябва да приеме един контролер са множество, то шансовете са, че искаме да вземем данни от формуляр чрез метод POST. Чрез специалните типове формуляри, които предоставя Symfony, можем да зададем бизнес правила на полетата в нашия формуляр и да кажем кой обект трябва да се напълни при валидно изпращане на формуляра в configureOptions() -> 'data_class'. Това означава, че трябва методът, който ще се извика при изпращане на формуляра да приема Request обект, с който формулярът ще взаимодейства (handle) за да напълни нашия data_class (иначе казано Binding model или какъв да е друг модел, например от базата данни).

В тази статия от cookbook-а на Symfony има стъпка по стъпка създаване на регистрационен формуляр - http://symfony.com/doc/current/doctrine/registration_form.html

Как в една конзола стартирахте едновременно и сървъра и doctrine? За да подкарам doctrine ми се налага да го пускам в друга конзола, така ли трябва?

 

Не съм. Или съм спирал сървъра с CTRL+C, писал съм doctrine команди и съм го пускал пак, или съм го правил от два отделни прозореца :)

За login ползваме ServiceController, а за Register - DefaultController. Защо различни?

 

ServiceController - не би трябвало да сме ползвали такова нещо. Но нямаме добра причина да ползваме два контролера. В повечето фреймуърци нещата свързани с оторизиране и други неща свързани със сигурност са изнесени в контролер наречен SecurityController, защото отговаря за сигурността. Затова и login е там. Докато регистрирането на потребител е просто INSERT операция на някакъв обект (в случая потребител - User), затова е и в контролерът, който отговаря за потребителите (UsersController).

 

Надявам се да съм отговорил. Ще си ги говорим нещата и утре на упражнението. Ако има нещо - не с колебайте да пишете в тази тема или да отворите нова :)

 

Поздрави,

Иван

Тагове:
1
PHP Web Development 06/12/2016 20:38:46
gabi.ivanova avatar gabi.ivanova 370 Точки

Ако може да покажеш с примери какви анотации се слагат в ентититата при различните връзки:

  • One to One Relationships
  • One to Many and Many to One Relationships
  • Many to Many Relationships
  • Self Referencing Relationships

 

0
ypetrov avatar ypetrov 4 Точки

На 20-та страница в документа за упражнение започва това, което търсиш според мен. (най-долу)

0
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки

Ето няколко примера: В ситуацията с нашия блог имахме статии (Article) и категории (Category).

Гледайки го от перспективата на статията, то много (many) статии могат да принадлежат на една (one) и съща категория. Тази връзка се нарича ManyToOne. Щом категорията е една, то трябва в нашия клас статия (Article) да имаме поле (наричано още навигационно пропърти) от тип Category.

Като код това в класа Article би изглеждало така:

    /**
     * @var AppBundle\Entity\Category
     *
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="articles")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;

Какво означава това? Това означава, че $category навигационното пропърти сочи към друга таблица от базата (targetEntity), която е тази с категориите (Category) и че в ответната таблица, навигационната колекция* (inversedBy, ще го разгледаме това по-късно) се казва статии "articles". Колоната в таблицата със статии (Article), която държи тази референция (която е външен ключ - foreign key) се казва "category_id" и реферира колоната (referencedColumnName) от таблицата с категории (Category), която се казва "id".

Щом много статии имат една и съща категория, това автоматична означава, че една (one) категория има много (many) статии. Голяма изненада, нали :) Едно и също нещо казах, но по два различни начина - просто второто е погледнато от переспетивата на категория (Category) - OneToMany.

Тогава в класът на категорията (Category) трябва да стои обърнатата (inversed) релация, така че когато си в категорията да имаш достъп до множеството статии, които тя има. Затова трябва някакво множество (колекция), и в категорията е нужно да има навигационно пропърти/колекция, което да се казва с името от inversedBy в статията, а именно "articles":

    /**
     * @var AppBundle\Entity\Article[]
     *
     * @ORM\OneToMany(targetEntity="Article", mappedBy="category")
     */
    private $articles;

Тук казваме, че имаме масив от статии, сочат към друга таблица (targetEntity), която е статия (Article) и се връзват за полето $category (от по-горното каре)

От тук нататък, когато вземеш някоя категория, за да й вземеш статиите в нея трябва да поискаш този масив :)

Това важи и за self-referencing relationship, т.е. ако една категория е дъщерна на друга категория да речем.

-

Сега да разгледаме един друг случай. Отново от нашия блог. Тагове. Дадена статия (Article) може да бъде тагната с определено множество от тагове (Tag), но друга статия може да има същото множество или подмножество/супермножество на това множество, което означава, че много статии (Many) могат да имат много тагове (Many) от перспективата на статия (но очевидно и от перспективата на таг).

В такъв случай имаме следната ManyToMany релация в статията:

    /**
     * @var AppBundle\Entity\Tag[]
     *
     * @ManyToMany(targetEntity="Tag")
     * @JoinTable(name="articles_tags",
     *      joinColumns={@JoinColumn(name="article_id", referencedColumnName="id")},
     *      inverseJoinColumns={@JoinColumn(name="tag_id", referencedColumnName="id")}
     *      )
     */
    private $tags;

Тук казваме, че множеството от обекти от тип Tag (targetEntity) ще е нужно да се извади от междинна таблица наречена "articles_tags". В тази междинна таблица ще има две колони: първата ще се казва article_id и ще сочи към "id" колоната в текущата таблица (Article), а втората ще се казва "tag_id" и ще сочи към колоната "id" в нашето targetEntity (Tag).

Погледнато от перспетивата на таговете обаче - положението е същото - таговете от своя страна имат множество статии (в смисъл много статии може да са тагнати от тага "PHP" да речем). Значи отново трябва да имаме колекция в Tag:

    /**
     * @var AppBundle\Entity\Article[]
     *
     * @ManyToMany(targetEntity="Article", mappedBy="tags")
     */
    private $articles;

Множеството от статии (колекцията от Article) сочи към Article (targetEntity) и се връзва за полето $tags в класа Article, като така знае, че трябва да ползва същите правила.

-

Да кажем, че трябва да намерим всички тагове на дадена статия. Тези навигационни колекции ни вършат много добра работа, защото няма нужда от вложени заявки или нещо от сорта, а е достатъчно да намерим статията и да използваме тази колекция:

$article = $articleRepository->findOne($id);
$tags = $article->getTags();
foreach ($tags as $tag) {
   $tagName = $tag->getName();
}

 

 

 

1
ypetrov avatar ypetrov 4 Точки

Седя и си блъскам главата с extend към extend-а на twig и нещо не се получава..

В момента имам base.html.twig, index.html.twig и register_form.html.twig, като всяко extend-ва предходното, ама последния нещо не може да разшири индекса, като му задавам да пълни {% block content %} който го има и в индекса също зададен за пъленене в <div>, който съм убеден, че трябва да се вижда.. май ще трябва и за динамичноста на системата да си поговорим. laugh

Още един проблем на точно единия час от лекцията в това видео: https://softuni.bg/trainings/resources/video/11380/video-screen-1-december-2016-ivan-yonkov-php-web-development-october-2016

създаваш форма от темплейтите.. обаче в моя сторм няма изобщо такива темплейти освен за два сървиса, това някакъв допълнителен плъгин ли е?

 

[ този проблем тука е Fixed - оказа се ,че трябва да добавя път до homepage и останалите се оправят сами, което е странно.. ] Опитвам се да ползвам и  {{ path('register') }}

което също нещо не работи като хората, ето какъв е резултата като седи по-горния път в линк: http://take.ms/dFOHR

Ползвам следния Route:  @Route("/register", name="register")

 

0
06/12/2016 02:23:25
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки

По първия въпрос за extend-ването, мисля че ако пишеш в един и същ блок, ще презаписваш content-а. Използвай други блокове в които да сложиш допълнителното съдържание.

Ако приемем, че имам base.html.twig в който има {% block main %}, то имам следното после:

    1. middle.html.twig:

{% extends 'base.html.twig' %}

{% block body %}
   <h1> Middle html</h1>
    {% block nested %}{% endblock %}
{% endblock %}

    2. last.html.twig

{% extends 'middle.html.twig' %}

{% block nested %}
   <h2>Nested content</h2>
{% endblock %}

И излизат и двата <hX> тага, както и нещата от main :)

 

По втория въпрос - пробвай url() вместо path()

0
ypetrov avatar ypetrov 4 Точки

Да проблема беше, че ползвах боди блока дори в третия темплейт, оказва се ,че наследника задължително трябва да е втори по веригата и не може да бъде трети от моята гледна точка.. може би довечера ще мога по-добре да го обясня.

 

Иначе втория ми въпрос беше във връзка създаването на формите от файловите темплейти, проблема с path-овете е решен също.

Ето какво е съдържанието при мен: http://take.ms/UCaAc

От документа с упражнения това, което при вас го има, а при мен го няма са на стр. 13 първа картинка.

0
krasimir.i.petrov avatar krasimir.i.petrov 11 Точки

Аз имам проблем.

Не мога да се справя с логина, мога но не по начина, който е показан в документа с упражненията. Правя всичко, както е описано там, рендирами се формата, но тя не работи.  

Иначе, аз съм си направил логин страница, която използва следните функции:

 /**

     * @Route("user/login", name="login")
     * @Method("GET")
     */
    public function login()
    {
        $form = $this->createForm(UserLogin::class);
        return   $this->render("users/login-profile.html.twig",[
            'form' => $form->createView()
        ]);

    }

    /**
     * @param Request $request
     * @Route("user/login", name="user_login_post")
     * @Method("POST")
     */
    public function loginPost(Request $request)
    {
        $user = new User();
        $postData = $request->request->all();



        $username = $postData['user_login']['username'];
        $password = $postData['user_login']['password'];
        if($username)
        {
           $user_exit_db=$this->getDoctrine()->getRepository(User::class);
            $user_exit=$user_exit_db->findOneBy(['username'=>$username]);
            if($user_exit && password_verify($password, $user_exit->getPassword()) )
            {
               echo "Hello, ". $user_exit->getUsername(). "!  You are ". $user_exit->getRoles().".";

                $session = new SessionService($user_exit->getId());
                $session->startSession($user_exit->getId());

             /*   $this->get('session')->start();
               $this->get('session')->set('id', $user_exit->getId());
              var_dump( $this->get('session')->get('id'));*/

             exit;
            }
            else
            {
                return $this->redirectToRoute("user_register");
            }
        }
        return $this->redirectToRoute("user_register");

    }

Така всичко си работи, но не съм сигурен, начина по който съм го направил дали е правилен.

 

Ясно ми е, че след успешен вход, трябва да подам информация на view, а то да рендира информация за потребителския профил, тук  съм го направил директно във функцията, просто за теста.

0
06/12/2016 21:34:48
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки

ОК е този вариант, но предполагам, че няма да може да се възползваш от @Security анотациите. Може да видиш дали по същия начин ги пише стандартното секюрити на симфони по сесиите, все пак.

Вариант е също като правиш Symfony проект, да цъкнеш Demo project чавката и да видиш как е направен логина там :)

0
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки
За players, planets, buildings и ships отделни контролери ли трябва да има?

 

Зависи от действията, но по-скоро да. Ето нещо примерно, което вчера разписвахме (файла, който обещах да кача, просто го пиша във форума):

Entity:
    User
    Building
    Unit
    Planet
    Resource
    Message

Controllers:
    UsersController:
        register(); users/register
        dashboard(); users/
    FlightController:
        index(); // current users flights for user's current planet id
        sendFleet(); // hostile coordinates
        sendFleetPost(); // sends fleet for real
    BuildingsController:
        index(); // current player buildings for user's current planet id
        build(); // starts buildings build process if possible (enough resources, etc...)
    ShipsController:
        index(); // current user ships for user's current planet id
        build(); // starts ships building process if possible  (enough resources, etc...)
    PlanetsController:
        change($id); // change to another planet of user's own

 

----

Message sending system
--
Entity:
class Message
{
    private $id;
    private $sender; // Entity\Player;
    private $receiver; // Entity\Player;
    private $text;
    private $sentOn; // \DateTime
    private $isRead; // boolean
}

$message = new Message();
$message->setSender($this->getUser());
$message->setReceiver($repo->findOne($recieverId));
$message->setText(...);
$message->setSentOn(new DateTime());
$message->setRead(false);
$em->save($message);
$em->flush();

 

-

Защо в примера с blog-а User наследява UserInterface, а като опитам Player да го наследи - не става?

 

Имплементирането на UserInterface изисква няколко метода да бъдат написани в класа. Когато ти подчертае с червено реда Player implements UserInterface му кликни ALT+ENTER и му дай да имплементира методите.

0
gabi.ivanova avatar gabi.ivanova 370 Точки

Когато правя връзка ManyToMany Users - Buildings, появява се междинна таблица с 2 колони uner_id - building_id, както си го обяснил. Обаче какво става, когато искам user с id 5 да има повече от една сграда с id 3, тоест да имам еднакви записи, а свързващата таблица няма id?

Вариант ли е да си направя отделна таблица, в която да държа user_id, building_id, count? И с всяка нова сграда ще се променя броя в таблицата?
Или е по-добре да имам таблица само с table_id, user_id, building_id, където ще може да има повторения? Или има по-добър начин, за който не се досещам?

0
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки

Ако искаш да го има още веднъж записа в ManyToMany може да сложиш на всеки JoinColumn че е unique=false.

Но защо ти трябва това? Не би ли трябвало за всяка планета/кралство да пазиш отделен запис, а не за потребител? Т.е. имаш planet_id | building_id. И по-скоро ти трябва трета колона за това на кое ниво е развил сградата, така че може би по-скоро тук ти трябва отделно ентити PlanetBuilding, което има Планета, Сграда и Ниво

0
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки
Каква точно е функцията на DI контейнера в блога, който създадохме, и как работи?

 

Работата на този контейнер сме я обяснявали многократно. Тук вероятно има няколко крити въпросителни, на които трябва да се отговори, за да се изясни картинката.

Ще започна по-отдалеч, пригответе се за по-дълго четене :))

 

Традиционно за да се създаде едно приложение ни е нужен просто компютър и език за програмиране :) РНР е идеален за това. С инсталирането на уеб сървър, нашите скриптове сложени в работната папка на сървъра, просто работят достъпвайки ги като файлове през адреса на сайта.

Което означава, че за да направим блог на нас ни трябва генерално три инструмента:

   1. Компютър с уеб сървър

   2. РНР скрипт

   3. База данни

Или иначе казано няма никакъв проблем да направя показването на таблица със статиите от един блог, редакцията на статия и т.н. без допълниелни абстракции и в един файл:

<?php
$pdo = new PDO("mysql:host=localhost;dbname=blog", "root", "");
$query = "SELECT * FROM articles";
$stmt = $pdo->query($query);
$articles = $stmt->fetchAll();
?>
<table border=1>
    <tr>
       <td>Id</td>
       <td>Title</td>
       <td>Body</td>
    </tr>
<?php foreach ($article as $articles): ?>
    <tr>
       <td><a href="articles.php?view_id=<?php echo $article['id']; ?>"><?php echo $article['id'];?></a></td>
       <td><?php echo $article['title']; ?></td>
       <td><?php echo $article['body']; ?></td>
    </tr>
<?php endforeach; ?>
<?php
if (isset($_GET['view_id'])) {
   $id = (int)$_GET['view_id'];
   $query = "SELECT * FROM articles WHERE id = ?";
   $stmt = $pdo->prepare($query);
   $stmt->execute([$id]);
   $artcle = $stmt->fetchRow();
?>
<div class="article_view">
    <span class="article_title"><?php echo $article['title']; ?></span>
    <span class="article_body"><?php echo $article['body']; ?></span>
<?php if ($_SESSION['id'] == $article['author_id']): ?>
    <span class="actions">
        <a href="articles.php?view_id=<?php echo $id; ?>&action=edit">Edit</a>
        <a href="articles.php?view_id=<?php echo $id; ?>&action=delete">Delete</a>
    </span>
<?php endif; ?>
</div>
<?php
   if (isset($_GET['id'])) {
      if ($_SESSION['id'] != $article['author_id']) {
          header("Location: access_denied.php?error=Not author");
          exit;
      }
?>
<form method="post" action="articles.php?view_id=<?php echo $id; ?>&action=edit">
    <input type="text" name="title" value="<?php echo $article['title']; ?>"/>
    <textarea name="body"><?php echo $article['body']; ?></textarea>
    <input type="submit" value="Edit!" name="edit"/>
</form>
<?php
            }       
        }    
    }     
}  // и божа работа още колко иф-а трябва да затворя
if (isset($_POST['edit'], $_GET['edit'], $_GET['id'])) {
   if ($_SESSION['id'] != $article['author_id']) {
          header("Location: access_denied.php?error=Not author");
          exit;
   }
   $query = "UPDATE articles SET title = ?, body = ? WHERE id = ?"
   $stmt = $pdo->prepare($query);
   $result = $stmt->execute([$_POST['title'], $_POST['body'], $_GET['id']]);
   if ($result) {
?>
       <h1>You have successfully edited the article</h1>
<?php
   }
}
?>
   

Казвам без ДОПЪЛНИТЕЛНИ абстракции, защото все пак ползваме някакви, като например: абстракция за връзка с база данни, функционалност за взимане на неща през браузърския вход, бази данни, уеб сървъри и т.н. (Leaky abstractions). Въпреки всички тези абстракции, кодът ни е доста конкретен.

Този код макар и може би работещ (не съм го тествал, написах го направо в поста) има достатъчно много проблеми, част от които ще споменем:

    1. Много от действията се повтарят и програмистът трябва постоянно да ги пише - заявки към база, подготвянето и изпращането на аргументи, контролирането на събитията (какво има въведето в $_GET, $_POST, за да вземе решение какво да направи), проверките дали си автор на статията и прочие

    2. Не е явно какво точно се опитва да направи кода, върши повече от едно нещо. Ако оригиналният му автор се върне след 2 месеца и трябва да внесе корекция, ще се забави доста. Ако се появят и други хора в екипа от програмисти, нещата ще станат по-сложни и с нарастването на проекта, ще нарастват и проблемите от същото естество

    3. Прекалено голямо е знанието по някои детайли, които трябва да има програмиста, за да внася корекции в кода или за да го пише въобще - какви са възможните колони в базата данни, какви са възможните ключове попълнени от потребителя в различни ситуации при $_GET и $_POST, какво е тяхното значение и кога трябва да се ползват. В примерът това е свързано само със статията, но в проект от мащабите на играта това са общо около 1000 колони, които трябва човек да запомни без да бърка.

    4. Внасянето на корекция на едно място ще изисква внасяне на корекция по целия код. Смяната на името на колоната "body" в "summary" ще наложи смяна по асоциативните масиви взети от заявките, ключовете от формулярите и UPDATE/INSERT заявките.

В заключение: На програмистът му се налага да пише безсмислени и повтаряеми действия и от един момент нататък изпитва трудност да внася корекции в кода или да добавя нови неща в него.

Това е проблем, с който са се сблъскали много хора и затова от толкова конкретен код, тръгват да изнасят абстракции, така че да решат тези проблеми. Първоначално програмистите искат да се съсредоточат върху логиката на приложението, а именно че това е блог и трябва да се създават, показват и редактират статии. А не искат постоянно да проверяват кой контролен параметър е вкаран в URL-а и колко неща трябва да се bind-нат в заявката. Това са неща, които на всяко действие трябва да се повторят и фокусът се измества от бизнес сценария на приложението в микро-повтаряеми задачи.

На помощ идва абстракция, която да се грижи за нас какво е въвел потребителя и къде да го прати и как да му даде данните ни - Routing.

Идва абстракция, която може да раздели презентацията от логиката и да раздели логиката на отделни части - Controller-и и View-та.

Идва на помощ абстракция, която превръща нашите данни от базата в строго-типизирани обекти, чиято репрезентация е реални полета в класове и няма нужда от помнене.

Докато писахме нашия блог, ние изнасяхме всичките тези абстракции една по една, така че да може да се възползваме от тях дългосрочно. Разбира се, изнасянето на тези абстракции отнема повече време от колкото самото създаване на блога, така че в конкретната ситуация е неоправдано, но знаем че за следващото приложение което правим можем да се възползваме от вече написаните абстракции.

За да се възползваме от тях, трябва да робуваме на определени правила, под които да пишем кода си. Т.е. ние докато пишем блога, написахме и работна рамка (Framework), която ни позволява по-лесно писане на проекти, като абстрактва за нас работата, върху която няма нужда да се фокусираме. Разбира се, практически писането на тази работна рамка е ненужно, тъй като топлата вода отдавна е открита, но това беше важна част за да видим как голяма част от магиите в популярните ткива рамки работят (до някаква степен) и какво да очакваме. Философията от по-горе се изповядва и от Symfony.

Та, връщайки се на въпроса какво прави DI Container-а в блога, който написахме, истината е, че DI container-ът не е част от блога, а от работната рамка (Framework-а).

Тъй като контролът в нашето приложение е обърнат и не сме ние тези, които се грижим за потребителския вход и какво да извикавме на база дадено събитие, а даден Framework (било то нашия или чужд), то създаването и управлението на най-близките до нас обекти (Контролери) е оставено в ръцете на Framework-а.

Framework-ът за нас създава обект от тип контролер и извиква метод от него, давайки ни достатъчно данни, за да можем да работим от този метод нататък. Това е чудесно, но този метод може да му се налага да свърши операция, която и друг метод м се налага да я върши. 

За да не говоря празни приказки, нека кажем, че методът за забравена парола трябва да изпрати email до определен потребител с определено съобщение. Тъй като искаме да знаем какви мейли сме пратили, искаме и да записваме мейлите в базата данни, така че методът трябва да направи и тази операция.

Когато обаче някой отговори на статия в блога на някой потребител, трябва отново да му изпратим мейл и да запишем мейла в базата данни.

Когато на някой потребител му настъпи рожденния ден, трябва отново да му пратим мейл и да запишем този мейл в базата данни.

От тук откриваме три места, в които имаме изпращане на email.

В такъв случай трябва да изнесем тази функционалност в отделен компонент, например клас. Класовете, които вършат конкретна задача, наричаме услуги (Services). Нека тогава кажем, че имаме MailService.

За да изпратим email обаче ни трябват настройки на mail server-а, подател и прочие. За да пишем в базата данни ни трябват конфигурациите за базата (потребител, парола...) или ни трябва обекта, който може да пише в базата. В такъв случай нашият MailService ще има нужда от тези зависимости.

Нека видим как ще изглежда изпращането на email през метода за забравена парола:

public function forgottenPassword(User $user)
{
    $db = new Database("root", "");
    $mailRepository = new MailRepository($db);
    $mailService = new MailService("smtp.royal.bg", "[email protected]", "123", $mailRepository);
    $mailService->setReceiver($user->getEmail());
    $mailService->setContent("......forgotten pwd....");
    $mailService->send();
}
   

Хм, нека видим как ще изглежда и при отговор на негов пост

public function answerPost(Comment $comment)
{
    $db = new Database("root", "");

    $articleRepository = new ArticleRepository($db);
    $article = $articleRepository->findOne($comment->getArticleId());
    $receiver = $article->getAuhor();

    $mailRepository = new MailRepository($db);
    $mailService = new MailService("smtp.royal.bg", "[email protected]", "123", $mailRepository);
    $mailService->setReceiver($receiver ->getEmail());
    $mailService->setContent("......someone replied to your article....");
    $mailService->send();
    // ...
}
   

Иронията тук е, че въпреки, че сме капсулирали нещата в класове и методи, повтаряме все пак голяма част от кода, освен това ръчно управляваме зависимостите и най-лошото - прекопираме конфигурационни настройки, които може да се променят в даден момент и да имаме проблем.

Както казахме по-рано, програмистът иска да се фокусира върху решаването на конкретната задача, а не върху разни досадни еднакви действия, като това: "Дай ми базата, дай ми мейл репото, дай ми мейл сървиса с ей тия настройки, аре ся прати мейл с това съдържание на този потребител", при положение че може просто да каже последните две: "прати мейл на този потребител с това съдържание"

И понеже имаме механизъм, който създава за нас контролел и вика негов метод, защо да нямаме и механизъм, който създавайки контролера да не провери от какво се нуждае този контролер и да му го даде?

Ако ние някъде сме дефинирали настройките на mail server-а на централно място и настройките на базата данни, няма никаква причина нашият централен механизъм да не мине през тях, да създаде нужните неща, за да съществува един MailService, който пък е нужен, за да създаде един Controller и респективно да подаде MailService на нашия контролер. Така имаме веднъж настройка какви зависимости ще има в приложението и после ползването им става без тези повтаряеми задачки:

class UsersController
{
    private $mailService;

    public function __construct(MailServiceInterface $mailService)
    {
        $this->mailService = $mailService;
    }

    public function forgottenPassword(User $user)
    {
        $this->mailService->setReceiver($user->getEmail());
        $this->mailService->setContent("......forgotten pwd....");
        $this->mailService->send();
    }
}
   
    

Container-ът се грижи за това да разреши зависимостите (рекурсивно*), така че да не ни се налага на нас да го правим ръчно и ни позволява да се фокусираме върху реалната работа, просто поисквайки дадена услуга. Също така ако се налага тези класове да бъдат създавани ръчно, то програмистът може да избере да им даде друг обект или същия но с други настройки, за да направи тестове или за да изпълни метода в друг смисъл (контекст).

* рекурсивно, защото някоя зависимост може да има три други зависимости, а всяка от тери три зависимости може да има например по други 2 и т.н.
 

0
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки
ОК ли е да имаме планета само на [x,y] координата или трябва да се дефинира като област?

 

Дано да съм разбрал правилно въпроса. Планетата си има "x", "y" - това е достатъчно за да се знае къде е, да.

Как е най-добре да се изкарврат грешките на потребителя, примерно за грешна парола, ако използваме framework-a, който ние писахме?

 

Хубаво е да се следва шаблона "Redirect After Post", т.е. когато формулярът бъде попълнен и изпратена и се извърши заявка POST, без значение как са се развили нещата да се направи Redirect. В такъв случай, за да се запазят съобщенията трябва да се запишат в Сесия.

Бяхме си писали един SessionInterface и може да го искаме в нашите контролери, за да вкараме съобщение под някакъв ключ, също така може в класа View да се поиска този интерфейс в конструктора, за да може после нашите View-та да имат достъп до него и да взимат съобщенията.

public function registerPost(UserRegisterBindingModel $model)
{
    if($model->getPassword() != $model->getConfirmPassword()) {
       $this->session->set('error', 'Passwords mismatch');
       $this->responseService->redirect("users", "register");
    }
    // ...
}

Съответно класа View:

class View implements ViewInterface
{
    private $session;
    private $mvcContext;

    public function __construct(SessionInterface $session, MVCContextInterface $mvcContext)
    {
        $this->session = $session;
        $this->mvcContext = $mvcContext;
    }

//...
}

И съответното view (users/register.php)

// ..
// ..
// ..
<div class="..."> .... </div>
// ..
// ..
<?php if($this->session->exists('error'): ?>
   <div class="error"> <?= $this->session->get('error'); ?> </div>
<?php endif; ?>
// ..
// ..

Така, разбира се, съобщенията ще останат в сесията постоянно, така че може да се направи някакъв метод, който взимайки съобщението го трие впоследствие (вместо директно get() да е друг метода, просто).

0
krasimir.i.petrov avatar krasimir.i.petrov 11 Точки

Аз да си питам. Не използвах вградения в Symfony security логин, а си направих мой, това проблем ли е?

0
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки

Не е проблем, щом можеш да си го ползваш и управляваш :)

0
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки

Здравейте,

Качил съм авторските решения от 8ми.

Също така направих и втора папка, в която съм разкачил контролерите и ползвам Service layer за операциите свързани с бизнес логиката.

Разбрах, че видеото засича на моменти, така че моля ако има неясни неща, да ги пишете тук и ще отговоря в писмен вид, за да разясня нещата, които са се изпуснали :)

Поздрави

 

0
12/12/2016 20:36:56
krasimir.i.petrov avatar krasimir.i.petrov 11 Точки

Може ли да попитам нещо? Необходимо ли е да разкачаме контролерите? Аз, част от логиката съм я изнесъл в импровизирани Service, но не знам до колко това е правилно. Ето какво съм направил. Регистрирал съм Service:

game_service:
        class: AppBundle\Services\GameService
        arguments: ['@doctrine.orm.entity_manager' ,'@service_container' ]

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

$create_planet=$this->get('game_service');
                $planet_name=$user->getUsername()."_planet";
                $create_planet->createnewPlanet($user, $planet_name);

 

Това всъщност ми трябваше на две места и за това го изнесах, да работи си и няма проблеми, но правилно ли е? 

Така хем имам някакъв сървис, обаче и не съм се разкачил от контролера.

0
13/12/2016 12:19:41
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки

Не е крайно грешно. Symfony я адвокират тази практика. Поне service-ите са ти отделни, Symfony казват, че няма проблем контролерите да са вързани за Framework-а. Аз не съм напълно на това мнение :) Бих казал, че е според ситуацията. Та в заключение - ОК е :)

0
RoYaL avatar RoYaL SoftUni Team Trainer 6845 Точки

Здравейте,

Малко насоки за защитите утре и на следващия ден :)

Тъй като имате ограничено време да защитавате ви предлагам следното нещо:

 

  1.  Намислете си какъв сценарий от проекта ще покажете
  2.  Подгответе си го разцъкан вече веднъж. Ако трябва регистрирайте потребители, създайте някакъв setup в играта
  3.  Като застанете отпред да защитавате - започнете с 30тина секунди контекст каква игра имате и какво се случва в нея и т.н. и дайте линк към GitHub.
  4.  Разкажете после през какъв сценарий ще ни прекарате за още 30тина сек, например "Ще регистрираме играч, ще влезем с друг играч и ще нападнем новорегистрирания......." и т.н.
  5.  Докато ни показвате сценария, пускайте от време на време IDE-то и показвайте части от кода на нещата, които показвате и смятате, че са интересни. Може предварително да си подготвите файловете, които ще покажете скролнати до фрагмента код, който ще покажете. Използвайте бялата тема на IDE-то и увеличете малко шрифта предварително.
  6.  В залата ползваме HDMI, ако се нуждаете от други adapter-и е хубаво да си носите. По принцип в сградата имаме, но има и други активности по това време и е възможно точно джаджата, която ви трябва да е заета.
  7.  Изтествайте си компютъра с HDMI или чрез adapter-а, вижте какви неща са нужни за да работи - резолюции, duplicate screen и други технически детайли. Идеята е да не се бавим с техническите детайли в часа на защитата, защото няма да ни стигне времето. Първия ден сме от 10:00 до 19:00 и това е в случай, че няма проблеми - иначе ще продължим до много късно, а университетът затваря по някое време, пък и няма да е приятно за хората след вас, ако се бавим :)
  8. Може да си оттренирате "talk"-а вкъщи, за да сте сигурни че се вмествате във времето (и че не говорите маловажни неща:)). Може някой да ви прекъсне с въпрос, имайте го предвид.
  9. Не пишете код 30 мин преди защитата ви, защото рискувате да счупите нещо :)

 

Успех на всички!

 

0
krazz avatar krazz 0 Точки

Би било добре ако може да предвидите адаптери от HDMI към VGA и HDMI към DisplayPort. Това са най-вероятните алтернативи за включване на тези които нямат HDMI порт или преходник към него.

0
16/12/2016 17:59:35