В последней части мы узнали о Neo4j и о том, как использовать его с PHP. В этой статье мы будем использовать эти знания для создания настоящего приложения социальной сети на базе Silex с графической базой данных.
Начальная загрузка приложения
Я буду использовать Silex , Twig , Bootstrap и NeoClient для создания приложения.
Создайте каталог для приложения. Я назвал мой spsocial
.
Добавьте эти строки в ваш composer.json
и запустите composer install
чтобы установить зависимости:
{ "require": { "silex/silex": "~1.1", "twig/twig": ">=1.8,<2.0-dev", "symfony/twig-bridge": "~2.3", "neoxygen/neoclient": "~2.1" }, "autoload": { "psr-4": { "Ikwattro\\SocialNetwork\\": "src" } } }
Вы можете скачать и установить Bootstrap в папку web/assets
вашего проекта.
Вы также можете найти демо-приложение начальной загрузки здесь: https://github.com/sitepoint-editors/social-network
Настройте приложение Silex
Нам нужно настроить Silex и объявить Neo4jClient, чтобы он был доступен в приложении Silex. Создайте файл index.php
в папке web/
вашего проекта:
<?php require_once __DIR__.'/../vendor/autoload.php'; use Neoxygen\NeoClient\ClientBuilder; $app = new Silex\Application(); $app['neo'] = $app->share(function(){ $client = ClientBuilder::create() ->addDefaultLocalConnection() ->setAutoFormatResponse(true) ->build(); return $client; }); $app->register(new Silex\Provider\TwigServiceProvider(), array( 'twig.path' => __DIR__.'/../src/views', )); $app->register(new Silex\Provider\MonologServiceProvider(), array( 'monolog.logfile' => __DIR__.'/../logs/social.log' )); $app->register(new Silex\Provider\UrlGeneratorServiceProvider()); $app->get('/', 'Ikwattro\\SocialNetwork\\Controller\\WebController::home') ->bind('home'); $app->run();
Twig настроен так, чтобы файлы его шаблонов находились в папке src/views
.
Домашний маршрут, указывающий на /
, зарегистрирован и настроен для использования WebController
мы создадим позже.
Структура приложения должна выглядеть так:
Обратите внимание, что здесь я использовал bower для установки начальной загрузки, но вам решать, что вы хотите использовать.
Следующим шагом является создание нашего базового макета с блоком контента, который наши дочерние шаблоны Twig будут переопределять своим собственным контентом.
Я возьму стандартную тему начальной загрузки с навигационной панелью вверху:
<!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"> <meta name="description" content=""> <meta name="author" content=""> <title>My first Neo4j application</title> <!-- Bootstrap core CSS --> <link href="{{ app.request.basepath }}/assets/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <![endif]--> <style> body { padding-top: 70px; } </style> </head> <body> <div class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" id="collbut" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">My first Neo4j application</a> </div> </div> </div> <div class="container-fluid"> {% block content %} {% endblock content %} </div> </body> </html>
Домашняя страница (поиск всех пользователей)
Пока что в приложении доступен Neo4j, создан наш базовый шаблон, и мы хотим перечислить всех пользователей на главной странице.
Мы можем достичь этого в два этапа:
- Создайте действие нашего
home
контроллера и получите пользователей из Neo4j - Передайте список пользователей в шаблон и перечислите их
Действие контроллера
<?php namespace Ikwattro\SocialNetwork\Controller; use Silex\Application; use Symfony\Component\HttpFoundation\Request; class WebController { public function home(Application $application, Request $request) { $neo = $application['neo']; $q = 'MATCH (user:User) RETURN user'; $result = $neo->sendCypherQuery($q)->getResult(); $users = $result->get('user'); return $application['twig']->render('index.html.twig', array( 'users' => $users )); } }
Контроллер показывает процесс, мы извлекаем neo
сервис и выдаем запрос Cypher для получения всех пользователей.
Затем коллекция пользователей передается в шаблон index.html.twig
.
Шаблон индекса
{% extends "layout.html.twig" %} {% block content %} <ul class="list-unstyled"> {% for user in users %} <li>{{ user.property('firstname') }} {{ user.property('lastname') }}</li> {% endfor %} </ul> {% endblock %}
Шаблон очень легкий, он расширяет наш базовый макет и добавляет несортированный список с именами и фамилиями пользователя в унаследованном блоке content
.
Запустите встроенный php-сервер и полюбуйтесь своей работой:
cd spsocial/web php -S localhost:8000 open localhost:8000
Особенности социальных сетей: отображение, за кем следует пользователь
Предположим теперь, что мы хотим нажать на пользователя и получить его подробную информацию и пользователей, за которыми он следует.
Шаг 1: Создайте маршрут в index.php
$app->get('/user/{login}', 'Ikwattro\\SocialNetwork\\Controller\\WebController::showUser') ->bind('show_user');
Шаг 2: Создать showUser
контроллера showUser
public function showUser(Application $application, Request $request, $login) { $neo = $application['neo']; $q = 'MATCH (user:User) WHERE user.login = {login} OPTIONAL MATCH (user)-[:FOLLOWS]->(f) RETURN user, collect(f) as followed'; $p = ['login' => $login]; $result = $neo->sendCypherQuery($q, $p)->getResult(); $user = $result->get('user'); $followed = $result->get('followed'); if (null === $user) { $application->abort(404, 'The user $login was not found'); } return $application['twig']->render('show_user.html.twig', array( 'user' => $user, 'followed' => $followed )); }
Рабочий процесс аналогичен любым другим приложениям, вы пытаетесь найти пользователя на основе логина.
Если он не существует, вы показываете страницу ошибки 404, в противном случае вы передаете пользовательские данные в шаблон.
Шаг 3: Создайте show_user
шаблона show_user
{% extends "layout.html.twig" %} {% block content %} <h1>User informations</h1> <h2>{{ user.property('firstname') }} {{ user.property('lastname') }}</h2> <h3>{{ user.property('login') }}</h3> <hr/> <div class="row"> <div class="col-sm-6"> <h4>User <span class="label label-info">{{ user.property('login') }}</span> follows :</h4> <ul class="list-unstyled"> {% for follow in followed %} <li>{{ follow.property('login') }} ( {{ follow.property('firstname') }} {{ follow.property('lastname') }} )</li> {% endfor %} </ul> </div> </div> {% endblock %}
Шаг 4: Рефакторинг списка пользователей на главной странице, чтобы показать ссылки на их профиль
{% for user in users %} <li> <a href="{{ path('show_user', { login: user.property('login') }) }}"> {{ user.property('firstname') }} {{ user.property('lastname') }} </a> </li> {% endfor %}
Обновите домашнюю страницу и нажмите на любого пользователя, чтобы отобразить его профиль и список подписавшихся пользователей.
Добавление предложений
Следующим шагом является предоставление предложений для профиля. Нам нужно немного расширить наш запрос шифрования в контроллере, добавив OPTIONAL MATCH
чтобы найти предложения, основанные на сети второй степени.
Необязательный префикс заставляет MATCH
возвращать строку, даже если не было совпадений, но с неразрешенными частями, установленными в null
(во многом как внешний JOIN) Поскольку мы потенциально получаем несколько путей для каждого друга-друга (fof), нам необходимо различать результаты, чтобы избежать дублирования в нашем списке (collect — это операция агрегации, которая собирает значения в массив):
Обновленный контроллер:
public function showUser(Application $application, Request $request, $login) { $neo = $application['neo']; $q = 'MATCH (user:User) WHERE user.login = {login} OPTIONAL MATCH (user)-[:FOLLOWS]->(f) OPTIONAL MATCH (f)-[:FOLLOWS]->(fof) WHERE user <> fof AND NOT (user)-[:FOLLOWS]->(fof) RETURN user, collect(f) as followed, collect(distinct fof) as suggestions'; $p = ['login' => $login]; $result = $neo->sendCypherQuery($q, $p)->getResult(); $user = $result->get('user'); $followed = $result->get('followed'); $suggestions = $result->get('suggestions'); if (null === $user) { $application->abort(404, 'The user $login was not found'); } return $application['twig']->render('show_user.html.twig', array( 'user' => $user, 'followed' => $followed, 'suggestions' => $suggestions )); }
Обновленный шаблон:
{% extends "layout.html.twig" %} {% block content %} <h1>User informations</h1> <h2>{{ user.property('firstname') }} {{ user.property('lastname') }}</h2> <h3>{{ user.property('login') }}</h3> <hr/> <div class="row"> <div class="col-sm-6"> <h4>User <span class="label label-info">{{ user.property('login') }}</span> follows :</h4> <ul class="list-unstyled"> {% for follow in followed %} <li>{{ follow.property('login') }} ( {{ follow.property('firstname') }} {{ follow.property('lastname') }} )</li> {% endfor %} </ul> </div> <div class="col-sm-6"> <h4>Suggestions for user <span class="label label-info">{{ user.property('login') }}</span> </h4> <ul class="list-unstyled"> {% for suggested in suggestions %} <li>{{ suggested.property('login') }} ( {{ suggested.property('firstname') }} {{ suggested.property('lastname') }} )</li> {% endfor %} </ul> </div> </div> {% endblock %}
Вы можете сразу же изучить предложения в вашем приложении:
Соединение с пользователем (добавление отношений)
Чтобы подключиться к предлагаемому пользователю, мы добавим ссылку на форму сообщения каждому предлагаемому пользователю, содержащую обоих пользователей в качестве скрытых полей. Мы также создадим соответствующий маршрут и действие контроллера.
Создание маршрута:
#web/index.php $app->post('/relationship/create', 'Ikwattro\\SocialNetwork\\Controller\\WebController::createRelationship') ->bind('relationship_create');
Действие контроллера:
public function createRelationship(Application $application, Request $request) { $neo = $application['neo']; $user = $request->get('user'); $toFollow = $request->get('to_follow'); $q = 'MATCH (user:User {login: {login}}), (target:User {login:{target}}) MERGE (user)-[:FOLLOWS]->(target)'; $p = ['login' => $user, 'target' => $toFollow]; $neo->sendCypherQuery($q, $p); $redirectRoute = $application['url_generator']->generate('show_user', array('login' => $user)); return $application->redirect($redirectRoute); }
Здесь нет ничего необычного, мы MATCH
начальный пользовательский узел и целевой пользовательский узел, а затем MERGE
соответствующие соотношения FOLLOWS
. Мы используем MERGE в отношениях, чтобы избежать повторяющихся записей.
Шаблон:
<div class="col-sm-6"> <h4>Suggestions for user <span class="label label-info">{{ user.property('login') }}</span> </h4> <ul class="list-unstyled"> {% for suggested in suggestions %} <li> {{ suggested.property('login') }} ( {{ suggested.property('firstname') }} {{ suggested.property('lastname') }} ) <form method="POST" action="{{ path('relationship_create') }}"> <input type="hidden" name="user" value="{{ user.property('login') }}"/> <input type="hidden" name="to_follow" value="{{ suggested.property('login') }}"/> <button type="submit" class="btn btn-success btn-sm">Follow</button> </form> <hr/> </li> {% endfor %} </ul> </div>
Теперь вы можете нажать кнопку « FOLLOW
для того пользователя, за которым вы хотите подписаться:
Удаление отношений:
Рабочий процесс для удаления отношений в значительной степени такой же, как и для добавления новых отношений, создания маршрута, действия контроллера и адаптации макета:
Маршрут:
#web/index.php $app->post('/relationship/remove', 'Ikwattro\\SocialNetwork\\Controller\\WebController::removeRelationship') ->bind('relationship_remove');
Действие контроллера:
public function removeRelationship(Application $application, Request $request) { $neo = $application['neo']; $user = $request->get('login'); $toRemove = $request->get('to_remove'); $q = 'MATCH (user:User {login: {login}} ), (badfriend:User {login: {target}} ) MATCH (user)-[follows:FOLLOWS]->(badfriend) DELETE follows'; $p = ['login' => $user, 'target' => $toRemove]; $neo->sendCypherQuery($q, $p); $redirectRoute = $application['url_generator']->generate('show_user', array('login' => $user)); return $application->redirect($redirectRoute); }
Вы можете видеть здесь, что я использовал MATCH
чтобы найти отношения между двумя пользователями,
и я добавил идентификатор для связи, чтобы иметь возможность DELETE
его.
Шаблон:
<h4>User <span class="label label-info">{{ user.property('login') }}</span> follows :</h4> <ul class="list-unstyled"> {% for follow in followed %} <li> {{ follow.property('login') }} ( {{ follow.property('firstname') }} {{ follow.property('lastname') }} ) <form method="POST" action="{{ path('relationship_remove') }}"> <input type="hidden" name="login" value="{{ user.property('login') }}"/> <input type="hidden" name="to_remove" value="{{ follow.property('login') }}"/> <button type="submit" class="btn btn-sm btn-warning">Remove relationship</button> </form> <hr/> </li> {% endfor %} </ul>
Теперь вы можете нажать кнопку Удалить связь под каждым подписанным пользователем:
Вывод
Графические базы данных идеально подходят для реляционных данных, и использовать их с PHP и NeoClient легко.
Cypher — это удобный язык запросов, который вы быстро полюбите, потому что он позволяет запрашивать ваш график естественным образом.
Использование графических баз данных для данных реального мира приносит большую пользу,
Я предлагаю вам узнать больше, прочитав руководство http://neo4j.com/docs/stable/ ,
взглянуть на примеры использования и примеры, предоставленные пользователями Neo4j, и следить за @ Neo4j в Twitter .