PHP
Article

Adding Social Network Features to a PHP App with Neo4j

By Christophe Willemsen

Graph Databases in PHP with Neo4j

In the last part, we learned about Neo4j and how to use it with PHP. In this post, we’ll be using that knowledge to build a real Silex-powered social network application with a graph database.

Bootstrapping the application

I’ll use Silex, Twig, Bootstrap and NeoClient to build the application.

Create a directory for the app. I named mine spsocial.

Add these lines to your composer.json and run composer install to install the dependencies :

{
  "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"
    }
  }
}

You can download and install Bootstrap to the web/assets folder of your project.

You can find the bootstrap demo app here as well: https://github.com/sitepoint-editors/social-network

Set up the Silex application

We need to configure Silex and declare Neo4jClient so it will be available in the Silex Application. Create an index.php file in the web/ folder of your project :

<?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 is configured to have its template files located in the src/views folder.
A home route pointing to / is registered and configured to use the WebController we will create later.
The application structure should look like this :

Imgur

Note that here I used bower to install bootstrap, but it is up to you what you want to use.

The next step is to create our base layout with a content block that our child Twig templates will override with their own content.
I’ll take the default bootstrap theme with a navbar on top:

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

The home page (retrieving all users)

So far, we have Neo4j available in the application, our base template is created and we want to list all users on the home page.

We can achieve this in two steps :

  • Create our home controller action and retrieve users from Neo4j
  • Pass the list of users to the template and list them

The Controller action

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

The controller shows the process, we retrieve the neo service and issue a Cypher query to retrieve all the users.
The users collection is then passed to the index.html.twig template.

The index template

{% 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 %}

The template is very light, it extends our base layout and add an unsorted list with the user’s firstnames and lastnames in the content inherited block.

Start the built-in php server and admire your work :

cd spsocial/web
php -S localhost:8000
open localhost:8000

Imgur

Social Network Features: Showing whom a user follows

Let’s say now that we want to click on a user, and be presented his detail information and the users he follows.

Step 1 : Create a route in index.php

$app->get('/user/{login}', 'Ikwattro\\SocialNetwork\\Controller\\WebController::showUser')
    ->bind('show_user');

Step 2: Create the showUser controller action

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

The workflow is similar to any other applications, you try to find the user based on the login.
If it does not exist you show a 404 error page, otherwise you pass the user data to the template.

Step 3 : Create the show_user template file

{% 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 %}

Step 4 : Refactor the list of users in the homepage to show links to their profile

{% for user in users %}
    <li>
        <a href="{{ path('show_user', { login: user.property('login') }) }}">
           {{ user.property('firstname') }} {{ user.property('lastname') }}
        </a>
    </li>
{% endfor %}

Refresh the homepage and click on any user for showing his profile and the list of followed users

Imgur

Adding suggestions

The next step is to provide suggestions to the profile. We need to slightly extend our cypher query in the controller by adding an OPTIONAL MATCH to find suggestions based on the second degree network.

The optional prefix causes a MATCH to return a row even if there were no matches but with the non-resolved parts set to null (much like an outer JOIN). As we potentially get multiple paths for each friend-of-a-friend (fof), we need to distinct the results in order to avoid duplicates in our list (collect is an aggregation operation that collects values into an array):

The updated controller :

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

The updated template :

{% 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 %}

You can immediately explore the suggestions in your application :

Imgur

Connecting to a user (adding relationship)

In order to connect to a suggested user, we’ll add a post form link to each suggested user containing both users as hidden fields. We’ll also create the corresponding route and controller action.

Creating the route :

#web/index.php

$app->post('/relationship/create', 'Ikwattro\\SocialNetwork\\Controller\\WebController::createRelationship')
    ->bind('relationship_create');

The controller action :

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

Nothing unusual here, we MATCH for the start user node and the target user node and then we MERGE the corresponding FOLLOWS relationship. We use MERGE on the relationship to avoid duplicate entries.

The template:

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

You can now click on the FOLLOW button of the suggested user you want to follow :

Imgur

Removing relationships :

The workflow for removing relationships is pretty much the same as for adding new relationships, create a route, a controller action and adapt the layout :

The route :

#web/index.php
$app->post('/relationship/remove', 'Ikwattro\\SocialNetwork\\Controller\\WebController::removeRelationship')
    ->bind('relationship_remove');

The controller action :

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

You can see here that I used MATCH to find the relationship between the two users,
and I added an identifier follows to the relationship to be able to DELETE it.

The template :

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

You can now click the Remove relationship button under each followed user :

Imgur

Conclusion

Graph databases are the perfect fit for relational data, and using it with PHP and NeoClient is easy.
Cypher is a convenient query language you will quickly love, because it makes possible to query your graph in a natural way.

There is so much benefit from using Graph Databases for real world data,
I invite you to discover more by reading the manual http://neo4j.com/docs/stable/ ,
having a look at use cases and examples supplied by Neo4j users and following @Neo4j on Twitter.

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Comments
Rush

Nice article, thanks. I'm interesting in graph database and Neo4j including. But have some questions about your article.
1) You show in article any suggestions, but i don't understand how they works (magic-magic). How can i customize it's algorithm. What if i need huge parametric select? Now we are using elasticsearch with some custom plugins for this purposes smile
2) What about performance, resources using and clasterization possibility?
3) And at all, can it be considered as an alternative to elasticsearch for example in fulltext/facet search?

ikwattro

Hi,

Thanks for reading the article.

1) No there is no magic, the suggestions are the nodes that you tell Cypher to return, you can also increase the relationship depth to provide friendOfFriend or friendOfFriendOfFriend suggestions.
Neo4j provide algorithms and it is working very well. However it is a graph database so its purpose is data store and lookup which is not data analysis. You can btw run advanced analysis like explained in this presentation : http://www.slideshare.net/bachmanm/recommendations-with-neo4j-fosdem-2015
2) Performance is always relative to how optimized is your model and how optimized are your queries. The Neo4j has better performance however when the graph can fit in memory, meaning that it is usual to have at least 16go memory reserved for the Java heap of the Neo4j server.
Clasterization or clusterization ? Neo4j Enterprise has an HighAvailibility mode, more informations in the documentation.
3) Currently, no, the fulltext feature are possible but are using an index that will be deprecated in the future. So before Neo4j 2.3 is released, ES is a common add-on we use with neo4j.

Rush

Ok, thanks for information.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.