Соцсеть. Проектирование и первые модели.

Так-так, а сейчас одна из самых важных частей разработки - проектирование.

Главный вопрос: какие данные будут использоваться и, что не менее важно, - как они будут использоваться.

Сойдемся на том, что мы сейчас не нацеливаемся на какую-то глобальную соцсеть, которая затмит фэйсбук, будем реалистами и спустимся на землю, где таких мечтателей и без нас навалом :) Зато мы разработаем сеть, скажем, для ограниченного круга людей или какого-то отдельного сферического города в вакууме. Не будем пока заморачиваться с большими нагрузками, сами понимаете, это уже отдельная область знаний, к которой мы еще вернемся позже.

Поскольку сеть у нас социальная, то без чего-чего, а без профилей пользователей она никак не обойдется, ну и возможность отправлять сообщения нынче нужна как воздух, да и дружбы дружить нужно вроде как. Поэтому в планировании и проектировании будем исходить из этих соображений.

Для начала сделаем профили со стенами, постами и лайками (здесь пока обойдемся стандартными PHP и MySQL, возможно для лайков переделаем под Redis, ну или не переделаем, а для развития рассмотрим ещё вариант), а для сообщений уже будем осваивать новые технологии ;)

Поехали. Сперва реализуем функционал авторизации. Стоп... чуть не забыл, чем хороши современные фреймворки. Там такое можно не писать, так как стандартные вещи вроде этой можно установить из коробки. Среди хороших программеров не принято изобретать велосипеды, тем более, что далеко не обязательно, что он получится лучше "заводского" =) К тому же, нам особо волноваться не придётся о вопросах безопасности и шифрования и т.п. (а если вы просто принцип понять хотите, то можете поискать в моих старых статьях, хоть они и старые, и в плане кода некоторые вещи писать уже не совсем грамотно, суть в целом не поменялась).

Но сперва я объясню, что такое миграции.

Миграции


В любом проекте, который чуть масштабнее, чем Hello world, с большой долей вероятности в ходе разработки меняется структура базы: добавляются поля, удаляются, меняется тип поля, таблицы новые появляются и т.д. И когда вы что-то изменили у себя на локалке, можете представить насколько неудобно их применять в других местах, особенно когда их много (на тестовом сервере, на компьютерах других разработчиков, на продакшене - везде ведь всё должно быть одинаково). Вот для этого и существуют миграции. Это php файлы, которые выполняют sql-команды, причем фреймворк сам отслеживает, какие из них уже запускались, появились ли новые, и если да, то запускает и их, и помечает их как выполненные.

Проще говоря, когда вы хотите что-то поменять, то создаете миграцию и всё =) При обновлении кода на других машинах останется только запустить их.

Так вот у нас изначально уже есть две миграции для обеспечения функционала авторизации, давайте запустим. Открываем консоль и пишем.

php artisan migrate

Здесь php - это программа, ей мы указываем, что хотим открыть файл artisan (это стандартный скрипт ларавеля для выполнения всяких команд), ну и дальше сама команда.

Как видим, у нас уже появились таблицы migrations, password_resets и users.

Просмотрев users, мы понимаем, что всё-то оно хорошо, но полей маловато.
Для авторизации хватит, но у нас же целые профили должны быть, а тут только name да email. Всё в порядке, поскольку профили нам нужны буквально на отдельной странице, то и хранить мы их можем в отдельной таблице, а здесь всё оставим пока как есть.

Нам ведь еще нужны страницы для входа и регистрации, но и для этого есть команда (т.к. функционал стандартный):

php artisan make:auth

Теперь у нас появилась возможность зайти на страницы /login и /register - их можно и изменить, но зачем?)
Тут задерживаться не будем, пойдем дальше. Прежде, чем создавать профили, расскажу немного об ORM для тех, кто не знает, что это.

Что такое ORM


Расшифровывается как Object Relational Mapper, не знаю, много ли вам это сказало, хе-хе. 

Если что, в математике в английском языке to map означает "associate each element of (a set) with an element of another set, be associated with or linked to". Так что всё логично, он связывает объекты вашего приложения с данными из реляционной базы данных.
Что очень важно, он позволяет не заморачиваться с запросами к БД (так как подобные вещи изначально прописаны в классе, от которого наследуют ваши объекты) и берет всю грязную работу на себя. Кстати, слово CRUD тоже пусть вас не пугает, если где-то встретите, так что ознакомьтесь на досуге. В общем, такие объекты для БД обычно называются models (а где-то entities). Приведу общий пример:

class User extends Model 
{ 
    public $table = 'users';
 
    public function getFullName()
    {
         return $this->name . ' ' . $this->last_name;
     }
}

И, скажем, чтобы получить из базы модель пользователя, делаем примерно так:

User::find(1);

Чтобы получить список:

User::all() или User::find()->where('active = 1')->all();

Чтобы создать:

$user = new User();
$user->name = 'Василий';
$user->save();

Вот примерно по такому принципу и строятся ORM.
Чем это выгодно? Тем же, чем вообще использование объектов. Вы можете прописать свои методы (как getFullName выше), наслаждаясь всеми прелестями инкапсуляции, можно где-то в качестве аргумента функции принимать строго определенный тип данных, наследование тоже иногда добавляет гибкости, при необходимости где-то можно использовать instanceof. Тут поле деятельности обширно, это лучше, чем просто иметь массив ['id' => 1, 'name' => 'Vasya', 'last_name' => 'Ivanov'] :)

Кстати, есть такой шаблон проектирования (или архитектурный паттерн) - Active Record, это я привел пример его. В Symfony 2 используется другой - Data Mapper. Там чуть посложнее: "слои" получения и сохранения сущностей отделены от самих сущностей, т.е. чтобы что-то получить или сохранить, нужно делать примерно так:

$repository = new UserRepository();
$users = $repository->findAll();
$user = new User();
$user->name = 'Василий';
$entityManager = new EntityManager();
$entityManager->save($user);

Но это для общего развития, не заморачивайтесь пока. А вообще рекомендую почитать что-нибудь о паттернах/шаблонах проектирования (design patterns). Я, например, читал книгу Мэтта Зандстры "ООП и шаблоны проектирования" (что-то такое).

Так вот, в Laravel тоже есть свой ORM (называется Eloquent), также есть возможность создавать свои модели (хотя это не всегда бывает обязательно, да и в некоторых случаях неоправданно, т.к. проще выполнить какой-то запрос так, по-старенькому, но до этого дойдем).

Короче, создаем модель профиля, а заодно и миграцию для создания таблицы. Посмотрев, в руководство, понимаем, что нам в консоли писать нужно так:

php artisan make:model UserProfile -m

Как видим, у нас создалась модель UserProfile, правда валяется она не очень удобно - прямо в папке app, ну да пофиг, перенесем вручную. И последующие модели будем генерировать в папку app/Models (чтобы не захламляли проект нам).
Поэтому создаем в app вышеназванную директорию, переносим туда файл, не забываем прописать верный namespace (раньше он был App, теперь становится App\Models).

Там по умолчанию добавляются поля created_at и updated_at, в которые заносятся время создания и изменения записи соответственно, так вот они бывают удобны, но в данном случае нам не нужны, поэтому из миграции мы удалим код с ними, а в модели добавим такой кусок.

public $timestamps = false;

Пока в профиль добавим только поле about типа TEXT, да, такой скромный профиль, потом остальное добавим. Запускаем миграцию...
ЧОРТ ПАДИРЫ! Нам же надо foreign key использовать!.. Ну как надо - совсем не обязательно, но здесь это будет очень уместно.

Немного о foreign keys


Итак, небольшое отступление, что же такое из себя представляют эти
foreign keys.

Если говорить коротко, то это ключ (как PRIMARY), который указывает на поле (тоже проиндексированное) в другой таблице, и нужен он для обеспечения целостности данных.
Пример будет нагляднее. Допустим у нас есть таблица пользователей users с полями id и name, и таблица их покупок с полями id, user_id, purchase.

users:
1 | Ivan
2 | Anton
3 | Ignat

purchases:
1 | 1 | book
2 | 1 | dvd
3 | 2 | bike
4 | 3 | tv
5 | 3 | camera

То есть понятное дело, что user_id неразрывно связан с id из таблицы users. И разумеется, мы не хотим, чтобы удалив пользователя, но забыв очистить список его покупок, мы потом столкнулись с тем, что у нас в базе лежат данные, не привязанные ни к кому. Мы, конечно, можем всё прописать в коде, но всегда есть вероятность что-то не учесть, что-то забыть, ведь и код постоянно меняется. Здесь и выходит на сцену FOREIGN KEY, предлагая нам возможность предусмотреть этот момент средствами БД (кстати, это актуально с движком InnoDB, MyISAM и прочие эту фичу не поддерживают).

Мы можем добавить ключ, который ссылается на первичный ключ в родительской таблице, например так:

ALTER TABLE purchases
 ADD FOREIGN KEY
 `FK_purchases_users` (`user_id`)
 REFERENCES users (`id`)
 [ON DELETE reference_option]
 [ON UPDATE reference_option]

ON DELETE - что делать с полем в дочерней таблице, если мы удаляем соответствующую запись из родительской таблицы.
ON UPDATE - если значение поля с ключом в родительской таблице изменилось.
Вместо reference_option можно подставить такие варианты, как:
CASCADE - делаем то же самое для дочерней записи: либо обновляем значение поля, либо удаляем запись.
RESTRICT - запрещаем что-то делать и выдаем ошибку.
NO ACTION - эквивалент для RESTRICT.
SET NULL - обнуляем поле, причем оно ОБЯЗАНО быть nullable, т.е. нельзя указать NOT NULL при создании таблицы. Эта опция бывает нужна, если мы хотим иметь возможность назначить что-то, чего может и не быть. Например, есть у нас 2 таблицы: с юзерами и с парковочными местами. Так вот место может быть занято кем-то, не занято никем, хозяин может потом поменяться.

Примеры:

Если в случае с покупками мы прописали ON DELETE CASCADE, то при удалении пользователя Ignat из таблицы purchases будут автоматически удалены tv и camera.

Допустим, что для парковок мы создали следующие таблицы:

users:
1 | Vasya
2 | Petr

parking_lots:
id | user_id | name
1 | 1 | Возле дома
2 | 2 | У сарая

Если мы для parking_lots.user_id зададим внешний ключ с ON DELETE SET NULL ON UPDATE CASCADE, то при удалении юзера Petr, поле parking_lots.user_id у записи "У сарая" станет null, а при изменении поля users.id = 3 у Васи, это же поле для записи "Возле дома" обновится - parking_lots.user_id тоже станет 3.

Ещё один момент: типы связываемых полей в родительской и дочерней таблицах должны полностью совпадать. Нельзя для одного задать INT(11), а для другого INT(10) NOT NULL - получите ошибку.

Вот ещё неплохое объяснение, гляньте как будет время.

А если вы не знаете, для чего вообще нужны индексы, то напишите в комментариях, и я дополню статью. Не могу всё разжевывать сразу, а то и так темпы медленные ;)

Вернемся же к нашим миграциям. Поскольку мы (ну как мы - я) слегка накосячили и сделали таблицу не такой, как следовало, то сейчас хотим откатить изменения, да-да, и для этого тоже есть команда.

php artisan migrate:rollback

Вам, между прочим, это делать нет нужды, т.к. скачаете вы уже финальный вариант, я просто демонстрирую процесс разработки =)
И кстати, миграции и роллбэки проходят batch-ами, кусками то бишь. Если вы создали 3 новые и накатили за раз, то при откате отменятся все три.

Дописываем всё, что необходимо и запускаем миграцию заново.

Возможно вы заметили, что я добавил в модели User и UserProfile какие-то функции с belongsTo() и hasOne()? Они нужны для связи моделей, чтобы мы потом в коде легко могли имея объект типа User $user обратиться к его профилю таким образом: $user->profile()->about.
О стандартных связях достаточно написано в документации, я не буду на них задерживаться, разве что если у вас с английским проблемы или там слишком сложно написано, тогда уж объясню, так и быть :)
А в целом такие штуки есть в любом себя уважающем ORM.

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

follower_id | followed_id

Всё просто: кто подписан, на кого подписан. А primary key будет составной, сразу включим оба поля в индекс. Дружбу будем проверять просто - чтобы тот, на кого вы подписаны, также был подписан на вас (но об этом тоже позже).

Сгенерируем миграцию без модели.

php artisan make:migration create_users_followers

Код я, к слову, буду выкладывать на гитхаб, коммит можно тут глянуть.

Вообще можете склонировать репозиторий себе на машину и периодически делать пулл, чтобы получать обновления, заодно будете видеть мою историю изменений (я стараюсь нормальные описания к коммитам писать, чтобы ясно было, что я делаю и зачем). Но советую мой код вытягивать в отдельную папку, а не ту, где вы работаете, чтобы вы не просто смотрели на код, а пробовали что-то и сами, руку набивали, в память заносили, ну вы поняли)

Так-с, что ещё? А, давайте для постов создадим табличку и модель.

Создаем посты


Сделаем пока необходимый минимум полей:

id - autoincrement
user_id - на чьей стене запись
content - текст записи
created_at & updated_at - пожалуй пригодятся
deleted_at - и вот здесь, пожалуй...

... расскажу-ка ещё о концепции soft deleting.

Иногда нужно, а иногда просто хочется, чтобы что-то из базы не удалялось совсем, а как-то отмечалось будто оно удалено (скажем, хотим оставить возможность восстановить, как удаленный профиль в контакте). Для этого в Eloquent уже реализована такая фича, как softDeletes, то есть не придется нам изобретать велосипед, что радует :) По сути, в таблице добавится nullable поле deleted_at, если там пусто, значит запись обычная, а если стоит timestamp, то удалена. Фреймворк сам это отслеживает и делает, что нужно. Мы всего-лишь в миграции указываем для таблицы:
$table->softDeletes();

а потом в сгенерированной модели

namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
 
class Post extends Model
{
     use SoftDeletes;
    protected $dates = ['deleted_at'];
}

Если вы вдруг не знаете, что за конструкция такая use SoftDeletes внутри класса, то обязательно почитайте про трейты (traits). Они во многих случаях позволяют добавить гибкости и избежать некоторых минусов наследования. А вообще, говоря простым языком, это как бы импорт набора методов под определенным именем, в нашем случае Illuminate\Database\Eloquent\SoftDeletes.

Что касается onDelete('cascade'): думаю норм, если при удалении юзера мы удаляем и все его записи, но если мы зачем-то хотим сохранить их, то тут лучше будет другой механизм продумать и использовать. Но нас и это устраивает.

Так, забыл, надо же ещё автора поста (кто вам на стену запостил), и репосты... До репостов мы ещё доберемся, а код миграции для добавления author_id приведен в этом коммите.

Поскольку автора может не быть, не забываем делать nullable, а еще если мы в FK используем on delete set null, то тем более обязательно nullable.

Так, что касается аватарок, мы еще прикинем, как нам лучше, а пока заглушку сделаем, чтобы выводилась какая-нибудь стандартная картинка.

Но таблица files нам все равно пригодится, поэтому создадим сейчас и её.

Поля:
int id - всё понятно
int user_id - чей файл
string filename - имя, которое было у файла при загрузке
string mime_type - его тип
string hash - хэш файла, по нему будем его получать, это можно сказать его новое имя будет
timestamp uploaded_at - когда загружен

Хэш делаем уникальным, но всякое бывает, так что в будущем этот момент предусмотрим. А пока выполним все миграции и сделаем передышку.

Папку .idea в .gitignore!


Если вы используете IDE как например phpStorm, у вас могла в корне проекта сгенерироваться папка .idea с настройками этой самой ide-шки, нам эти файлы отслеживать в системе контроля версий совершенно не нужно, поэтому добавим их (всю папку) в .gitignore.

Как это делать, я описывал в предыдущей статье.

IDE helper


Опять же, если вы пользуетесь phpStorm, то могли заметить, что он справа подсвечивает вызовы многих классов как ошибочные (из-за использования т.н. фасадов, pattern Facade), и не делает автодополнений, которые в целом ускоряли бы процесс. Если вас это не напрягает, то можете пропустить этот  пункт, в противном случае рекомендую установить
IDE Helper.

Переходим в консоли в папку проекта, и если у вас всё настроено как надо, то достаточно написать

composer require barryvdh/laravel-ide-helper:v2.1.2 -n --no-progress

Если компоузер вы не устанавливали, а использовали phar-файл, то можете вообще полные пути указать к php и composer.phar

D:\OpenServer\modules\php\PHP-5.5\php.exe D:\OpenServer\modules\php\PHP-5.5\composer.phar require barryvdh/laravel-ide-helper:v2.1.2 -n --no-progress

После запуска у вас должны скачаться все зависимости. В файл config/app.php остается добавить одну строку в список service provider'ов "Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class" и выполнить (пока) одну из возможных команд:

php artisan ide-helper:generate

Как выложить код на GitHub или Bitbucket


Когда вы работаете в команде, вам всем нужно иметь возможность делиться кодом, чтобы был доступ к одному общему репозиторию
 (так сказать, хранилищу)Вы, конечно, можете установить свой git-сервер, а есть различные бесплатные хостинги для репозиториев вашего кода.

Переходим на один из таких сайтов. Находим какую-нибудь кнопки вроде New repository, пишем туда имя (Repository name)

А на следующем шаге нам сразу выдают URL этого самого свежесозданного удаленного (aka внешнего) репозитория.

Его вы можете использовать при создании / клонировании репозитория на локальной машине. Но я например, если помните, сперва создал его, ничего не указывая, и сейчас мне нужно добавить данные об удаленном репозитории. Поэтому в SourceTree открываю окно "Настройки репозитория" > "Внешние", жму "Добавить".

Затем ввожу полученный URL, имя пользователя и нажимаю OK. Теперь мы можем делать пуши и пуллы и отслеживать состояния веток.

Собственно, со своей задачей я не справился (обещал, что статья будет о проектировании, а проектировали мы по минимуму), ну и ладно %)

Код можно смотреть вот здесь. Кстати ВАЖНО: если где-то в коммитах найдете email моего брата, прошу, не пишите ему письма с вопросами, он очень занят. Лучше оставляйте комментарии :)

Да, я не могу рассказать всё, и где-то возможно что-то затрагиваю поверхностно, но моя цель - дать вам понимание, и если что-то совсем плохо объяснил, напишите ниже, по возможности обновлю статью ;)

 Жду с нетерпением
ваших комментариев!
 

Подписаться на RSS

Вы можете нажать "подписаться", чтобы следить за моими новостями!
Так вы всегда будете в курсе появления новостей на сайте =)
О том, что такое RSS можно прочитать здесь.

Подписаться

Подписаться на Twitter

Я специально зарегистрировался в Твиттере, чтобы вы могли следить за обновлениями на сайте =)

Подписаться

Envato marketplace А эти люди занимаются прокатом карнавальных костюмов и масок в Минске. К слову, я им делал сайт.