Adding Social Network Features to a PHP App with Neo4j

Share this article

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.

Frequently Asked Questions about Adding Social Network Features to PHP App with Neo4j

What is Neo4j and why is it important in PHP applications?

Neo4j is a highly scalable, native graph database that is designed to leverage data relationships. It is important in PHP applications because it allows developers to store, manage, and query data with its graph data model. With Neo4j, you can handle and analyze high volumes of data in real-time, making it ideal for building social network features in PHP applications.

How can I install and configure Neo4j for my PHP application?

To install Neo4j, you need to download the latest version from the official website and follow the installation instructions. Once installed, you can configure it by editing the configuration file, usually located in the ‘conf’ directory of your Neo4j installation. You can then connect it to your PHP application using a Neo4j PHP client.

What are the new features in PHP 7.4, 8.1, and 8.3 that can enhance my application?

PHP 7.4 introduced typed properties, arrow functions, and preloading, among other features. PHP 8.1 brought enums, fibers, and read-only properties. PHP 8.3, although not yet released, is expected to introduce new features that will further enhance your application. These features can improve the performance, readability, and maintainability of your PHP application.

How can I add social network features to my PHP application using Neo4j?

Adding social network features to your PHP application using Neo4j involves creating a graph database, defining the relationships between the nodes, and querying the database. You can use Cypher, Neo4j’s query language, to interact with the graph database from your PHP application.

What are the benefits of using Neo4j over other databases for social network features?

Neo4j, as a graph database, is inherently more suitable for social network features than other types of databases. It allows for efficient querying and handling of complex relationships, which are common in social networks. It also provides high performance, scalability, and flexibility.

How can I migrate my PHP application to a newer version?

Migrating your PHP application to a newer version involves updating your PHP installation, updating your code to remove deprecated features and use new ones, and testing your application to ensure it works correctly. It’s recommended to do this in a development environment first before applying the changes to the production environment.

What are the common challenges when adding social network features to a PHP application and how can I overcome them?

Some common challenges include handling large volumes of data, managing complex relationships, and ensuring real-time performance. These can be overcome by using a suitable database like Neo4j, optimizing your queries, and using efficient data structures and algorithms.

How can I optimize the performance of my PHP application with Neo4j?

You can optimize the performance of your PHP application with Neo4j by using efficient queries, indexing your data, and using Neo4j’s in-built performance tuning features. You can also use PHP’s performance-enhancing features, such as JIT compilation and preloading.

How can I secure my PHP application with Neo4j?

You can secure your PHP application with Neo4j by using secure connections, implementing authentication and authorization, and using Neo4j’s in-built security features. You should also follow best practices for PHP security, such as validating input, using prepared statements, and keeping your PHP installation up-to-date.

Where can I find resources to learn more about using Neo4j with PHP?

You can find resources on the official Neo4j and PHP websites, including documentation, tutorials, and community forums. There are also many online courses, books, and blogs that cover this topic in depth.

Christophe WillemsenChristophe Willemsen
View Author

Christophe worked as Software Engineer for almost 15 years in maritime industries and at the NATO. He is now Neo4j Expert and trainer at GraphAware, the #1 Neo4j's consulting partner. Among eating graphs everyday, he likes to develop applications with the Symfony components or the full stack framework.

BrunoSgraphgraph databaseneo4jOOPHPPHPsilexsocial network
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week