Статьи

Добавление функций социальных сетей в приложение PHP с Neo4j

В последней части мы узнали о 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 мы создадим позже.
Структура приложения должна выглядеть так:

Imgur

Обратите внимание, что здесь я использовал 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 

Imgur

Особенности социальных сетей: отображение, за кем следует пользователь

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

Шаг 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 %} 

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

Imgur

Добавление предложений

Следующим шагом является предоставление предложений для профиля. Нам нужно немного расширить наш запрос шифрования в контроллере, добавив 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 %} 

Вы можете сразу же изучить предложения в вашем приложении:

Imgur

Соединение с пользователем (добавление отношений)

Чтобы подключиться к предлагаемому пользователю, мы добавим ссылку на форму сообщения каждому предлагаемому пользователю, содержащую обоих пользователей в качестве скрытых полей. Мы также создадим соответствующий маршрут и действие контроллера.

Создание маршрута:

 #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 для того пользователя, за которым вы хотите подписаться:

Imgur

Удаление отношений:

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

Маршрут:

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

Теперь вы можете нажать кнопку Удалить связь под каждым подписанным пользователем:

Imgur

Вывод

Графические базы данных идеально подходят для реляционных данных, и использовать их с PHP и NeoClient легко.
Cypher — это удобный язык запросов, который вы быстро полюбите, потому что он позволяет запрашивать ваш график естественным образом.

Использование графических баз данных для данных реального мира приносит большую пользу,
Я предлагаю вам узнать больше, прочитав руководство http://neo4j.com/docs/stable/ ,
взглянуть на примеры использования и примеры, предоставленные пользователями Neo4j, и следить за @ Neo4j в Twitter .