SSR React with Symfonyfor the Strong of Spirit Workshop

Kick off

Open this website in your browser: http://sfreact.limenius.com/.
The slides for the sections of theory are available at https://www.slideshare.net/nachomartin/server-side-rendering-with-react-and-symfony.
Theory
Introduction to the workshop.

A first program

Let's start from the beginning.
Create a new directory somewhere and init a composer project in it:
cd
mkdir sfreact
cd sfreact
composer init
Answer with the default option (or what you want, it doesn't really matter), except for the last two questions:
Would you like to define your dependencies (require) interactively [yes]? no
Would you like to define your dev dependencies (require-dev) interactively [yes]? no
And install this single package:
composer require symfony/process
Let's play with this new project 🎉
Let's write our first SSR code, open a file called index.php and add this code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace App;

require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Process\Process;

$code = <<<JS
1+1;
JS;

$embedded = '"process.stdout.write(eval(\''.$code.'\').toString())"';

$process = new Process('node -e '.$embedded);

$process->run();

echo('<html><body>');

echo('My program is: '.$embedded.'<br/>');
echo('Output: '.$process->getOutput().'<br/>');

echo('</body></html>');
Run the embedded server of PHP with
php -S localhost:8080
And visit http://localhost:8080 in your browser.
You should see something like
1
2
My program is: "process.stdout.write(eval('1+1;').toString())"
Output: 2
Let's examine these parts. They are simple but important:
  • eval(myprogram)
  • toString()
  • process.stdout.write()
This is called a runner. This runner has many limitations. Let's address them one by one:

Error handling

Discuss
What happens if we change $code to instead of being
"1 + 1"
something like
"1 + i_am_not_defined"
?
Run your program with
node -e "<program>"
in the command line, to see what should happen.
Let's add error handling to our little program:
1
2
3
4
echo('My program is: '.$embedded.'<br/>');
echo('Exit code: '.$process->getExitCode().'<br/>');
echo('Output: '.$process->getOutput().'<br/>');
echo('Error Output: '.$process->getErrorOutput().'<br/>');
1
2
3
4
My program is: "process.stdout.write(eval('1+i_am_not_defined;').toString())"
Exit code: 1
Output: 
Error Output: undefined:1 1+i_am_not_defined; ^ ReferenceError: i_am_not_defined is not defined at eval (eval at ([eval]:1:22), :1:3) at [eval]:1:22 at Script.runInThisContext (vm.js:91:20) at Object.runInThisContext (vm.js:298:38) at Object. ([eval]-wrapper:6:22) at Module._compile (internal/modules/cjs/loader.js:689:30) at evalScript (internal/bootstrap/node.js:563:27) at startup (internal/bootstrap/node.js:248:9) at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)
There is much more we could do here, but at least this will give us some feedback to move on.

Multiline support

Let's try another silly problem. Can we run this code?
1
2
3
4
5
6
$code = <<<JS
var sum = function(a, b) {
  return a + b;
}
sum(1, 1);
JS;
Exercise
Run this code.
The problem is that we are writing a multiline program and running it with node -e. We will also experience problems when our program is big enough to not fit the command line restrictions (a limitation that varies depending on the OS).
We could for instance write a temporary file.
Let's try this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php

namespace App;

require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Process\Process;


function createTemporaryFile($content = null, $extension = null)
{
    $dir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR);
    if (!is_dir($dir)) {
        if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
            throw new \RuntimeException(sprintf("Unable to create directory: %s\n", $dir));
        }
    } elseif (!is_writable($dir)) {
        throw new \RuntimeException(sprintf("Unable to write in directory: %s\n", $dir));
    }
    $filename = $dir.DIRECTORY_SEPARATOR.uniqid('ssr_', true);
    if (null !== $extension) {
        $filename .= '.'.$extension;
    }
    if (null !== $content) {
        file_put_contents($filename, $content);
    }
    return $filename;
}

$code = <<<JS
var sum = function(a, b) {
  return a + b;
}
sum(1, 1);
JS;

$embedded = 'process.stdout.write(eval('.json_encode($code).').toString())';

$filename = createTemporaryFile($embedded, 'js');

$process = new Process('node '.$filename);

$process->run();

echo('<html><body>');

echo('My program is: '.$embedded.'<br/>');
echo('Filename : '.$filename.'<br/>');
echo('Exit code: '.$process->getExitCode().'<br/>');
echo('Output: '.$process->getOutput().'<br/>');
echo('Error Output: '.$process->getErrorOutput().'<br/>');

echo('</body></html>');
Apart from creating a temporary file (that we should take care to remove in a real environment), this line has changed
$embedded = '"process.stdout.write(eval(\''.$code.'\').toString())"';
To this:
$embedded = 'process.stdout.write(eval('.json_encode($code).').toString())';

Variant and Context code

Typically we will face this situation. We will want to have two pieces of code:
  • A (rather long) program that doesn't change between requests, or context code.
  • A (rather short) program that changes between requests, or variant code.
Let's express it. Change this part:
1
2
3
4
5
6
$code = <<<JS
var sum = function(a, b) {
  return a + b;
}
sum(1, 1);
JS;
To this:
1
2
3
4
5
6
7
8
9
10
11
$context = <<<JS
var sum = function(a, b) {
  return a + b;
};
JS;

$variant = <<<JS
sum(1, 1);
JS;

$code = $context . $variant;
This expresses what we will want to do. We will have a (rather big) application, and in each request we will want to say "hey, app, render this component with these parameters".
Let's load the context part from a file:
1
$context = file_get_contents(__DIR__.DIRECTORY_SEPARATOR.'app.js');
And place the context code in app.js:
1
2
3
var sum = function(a, b) {
  return a + b;
};

PhpExecJs

We could refine our runner more and more... or we can use a library that does this for you. Your instructor in this workshop has written such library
Apart from what we were doing, it will use the PHP extension V8js if it is installed.
This extension is hard to compile, however, it comes with a huge gain in performance and the ability of caching the context between requests.
Install it:
composer require nacmartin/phpexecjs
And rewrite our PHP file to be:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

namespace App;

require __DIR__ . '/vendor/autoload.php';

use Nacmartin\PhpExecJs\PhpExecJs;

$phpexecjs = new PhpExecJs();

$phpexecjs->createContextFromFile(__DIR__.DIRECTORY_SEPARATOR.'app.js');

$variant = <<<JS
sum(1, 1);
JS;

$result = $phpexecjs->evalJs($variant);

echo('<html><body>');

echo('Output: '.$result);

echo('</body></html>');
Does it still work?
Now we are ready to build our app in the context part. We will want lots of things! Let's see:
  • Dependency management.
  • Modern JS or JSX or whatever.
  • All bundled in one file.
  • Something that we can use for our client side app.
  • ...and thousand of other things.
There is a tool that does all of this, so let's give it a short introduction in the next chapter.
Got lost?
The code up to this point is in the tag 01-kickoff of the repository https://github.com/Limenius/workshop-symfony-react.git
This means:
git reset HEAD --hard
(To discard your changes)
And then:
git checkout 01-kickoff