├── .gitignore
├── DependencyInjection
├── Compiler
│ └── CacheCompilerPass.php
├── Configuration.php
└── LimeniusReactExtension.php
├── LICENSE
├── LimeniusReactBundle.php
├── README.md
├── Resources
├── config
│ ├── services.xml
│ └── twig.xml
└── doc
│ └── index.md
├── composer.json
└── ruleset.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.lock
2 | vendor/*
3 | studio.json
4 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/CacheCompilerPass.php:
--------------------------------------------------------------------------------
1 | getParameter('limenius_react.cache_enabled')) {
15 | return;
16 | }
17 | $appCache = $container->findDefinition('cache.app');
18 | $key = $container->getParameter('limenius_react.cache_key');
19 | $renderer = $container
20 | ->getDefinition('limenius_react.phpexecjs_react_renderer')
21 | ->addMethodCall('setCache', [$appCache, $key]);
22 |
23 | $container
24 | ->getDefinition('limenius_react.render_extension')
25 | ->addMethodCall('setCache', [$appCache]);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode();
21 | } else {
22 | // BC layer for symfony/config 4.1 and older
23 | $rootNode = $treeBuilder->root('limenius_react');
24 | }
25 | $rootNode
26 | ->children()
27 | ->enumNode('default_rendering')
28 | ->values(array('server_side', 'client_side', 'both'))
29 | ->defaultValue('both')
30 | ->end()
31 | ->arrayNode('serverside_rendering')
32 | ->addDefaultsIfNotSet()
33 | ->children()
34 | ->booleanNode('fail_loud')
35 | ->defaultFalse()
36 | ->end()
37 | ->booleanNode('trace')
38 | ->defaultFalse()
39 | ->end()
40 | ->enumNode('mode')
41 | ->values(array('phpexecjs', 'external_server'))
42 | ->defaultValue('phpexecjs')
43 | ->end()
44 | ->scalarNode('server_bundle_path')
45 | ->defaultNull()
46 | ->end()
47 | ->scalarNode('server_socket_path')
48 | ->defaultNull()
49 | ->end()
50 | ->arrayNode('cache')
51 | ->addDefaultsIfNotSet()
52 | ->children()
53 | ->booleanNode('enabled')
54 | ->defaultFalse()
55 | ->end()
56 | ->scalarNode('key')
57 | ->defaultValue('app')
58 | ->end()
59 | ->end()
60 | ->end()
61 | ->end()
62 | ->end()
63 | ->end();
64 |
65 | return $treeBuilder;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/DependencyInjection/LimeniusReactExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration($configuration, $configs);
24 |
25 | $container->setParameter('limenius_react.default_rendering', $config['default_rendering']);
26 | $container->setParameter('limenius_react.fail_loud', $config['serverside_rendering']['fail_loud']);
27 | $container->setParameter('limenius_react.trace', $config['serverside_rendering']['trace']);
28 |
29 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
30 | $loader->load('services.xml');
31 | $loader->load('twig.xml');
32 |
33 | $serverSideEnabled = $config['default_rendering'];
34 | if (in_array($serverSideEnabled, array('both', 'server_side'), true)) {
35 | $serverSideMode = $config['serverside_rendering']['mode'];
36 | if ($serverSideMode === 'external_server') {
37 | if ($serverSocketPath = $config['serverside_rendering']['server_socket_path']) {
38 | $container
39 | ->getDefinition('limenius_react.external_react_renderer')
40 | ->addMethodCall('setServerSocketPath', array($serverSocketPath))
41 | ;
42 | }
43 | $renderer = $container->getDefinition('limenius_react.external_react_renderer');
44 | } else {
45 | if ($serverBundlePath = $config['serverside_rendering']['server_bundle_path']) {
46 | $container
47 | ->getDefinition('limenius_react.phpexecjs_react_renderer')
48 | ->addMethodCall('setServerBundlePath', array($serverBundlePath))
49 | ;
50 | }
51 | $renderer = $container->getDefinition('limenius_react.phpexecjs_react_renderer');
52 | }
53 | $container->setDefinition('limenius_react.react_renderer', $renderer);
54 | }
55 |
56 | $cache = $config['serverside_rendering']['cache'];
57 | $container->setParameter('limenius_react.cache_enabled', $cache['enabled']);
58 | if ($cache['enabled']) {
59 | $container->setParameter('limenius_react.cache_key', $cache['key']);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Nacho Martín
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LimeniusReactBundle.php:
--------------------------------------------------------------------------------
1 | addCompilerPass(new CacheCompilerPass());
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReactBundle
2 |
3 | ReactBundle integrates [ReactRenderer](https://github.com/Limenius/ReactRenderer) with Symfony. This lets you implement React.js client and server-side rendering in your Symfony projects, allowing the development of universal (isomorphic) applications.
4 |
5 | **Note**: If you are new to React.js, please note that this bundle is not by any means required to use React with Symfony. This allows you to do some advanced features such as Server Side Rendering, or injecting components directly from Twig tags.
6 |
7 | Features include:
8 |
9 | * Prerender server-side React components for SEO, faster page loading, and users that have disabled JavaScript.
10 | * Twig integration.
11 | * Client-side render will take the server-side rendered DOM, recognize it, and take control over it without rendering again the component until needed.
12 | * Error and debug management for server and client side code.
13 | * Simple integration with Webpack.
14 |
15 | [](https://packagist.org/packages/limenius/react-bundle)
16 | [](https://packagist.org/packages/limenius/react-bundle)
17 | [](https://packagist.org/packages/limenius/react-bundle)
18 |
19 | # Example
20 |
21 | For a complete example, with a sensible Webpack set up and a sample application to start with, check out [Symfony React Sandbox](https://github.com/Limenius/symfony-react-sandbox).
22 |
23 | # Documentation
24 |
25 | The documentation for this bundle is available in the `Resources/doc` directory of the bundle:
26 |
27 | * Read the [LimeniusReactBundle documentation](https://github.com/Limenius/ReactBundle/blob/master/Resources/doc/index.md)
28 |
29 | # Installation
30 |
31 | All the installation instructions are located in the documentation.
32 |
33 | # License
34 |
35 | This bundle is under the MIT license. See the complete license in the bundle:
36 |
37 | LICENSE.md
38 |
39 | # Credits
40 |
41 | ReactBundle is heavily inspired by the great [React On Rails](https://github.com/shakacode/react_on_rails), and uses its npm package to render React components.
42 |
43 | The installation instructions have been adapted from [https://github.com/KnpLabs/KnpMenuBundle](https://github.com/KnpLabs/KnpMenuBundle). Because they were great.
44 |
45 | # With Silex
46 |
47 | Silex was discontinued in June 2018. However, if you wish to use ReactRenderer with Silex, check out @teameh [Silex React Renderer Service Provider](https://github.com/teameh/silex-react-renderer-provider).
48 |
--------------------------------------------------------------------------------
/Resources/config/services.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Limenius\ReactRenderer\Renderer\ExternalServerReactRenderer
9 | Limenius\ReactRenderer\Renderer\PhpExecJsReactRenderer
10 | Limenius\ReactRenderer\Context\SymfonyContextProvider
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | unix://%kernel.project_dir%/var/node.sock
19 | %limenius_react.fail_loud%
20 |
21 |
22 |
23 |
24 | %kernel.project_dir%/var/webpack/server-bundle.js
25 | %limenius_react.fail_loud%
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Resources/config/twig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Limenius\ReactRenderer\Twig\ReactRenderExtension
9 |
10 |
11 |
12 |
13 |
14 |
15 | %limenius_react.default_rendering%
16 | %limenius_react.trace%
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Resources/doc/index.md:
--------------------------------------------------------------------------------
1 | Using ReactBundle
2 | ===================
3 |
4 | *Where we explain how to install and start using ReactBundle*
5 |
6 | Installation
7 | ------------
8 |
9 | First and foremost, note that you have a complete example with React, Webpack and Symfony Standard Edition at [Limenius/symfony-react-sandbox](https://github.com/Limenius/symfony-react-sandbox) ready for you. Feel free to clone it, run it, experiment, and copy the pieces you need to your project. Being this bundle a frontend-oriented bundle, you are expected to have a compatible frontend setup.
10 |
11 | ### Step 1: Download the Bundle
12 |
13 | Open a command console, enter your project directory and execute the
14 | following command to download the latest stable version of this bundle:
15 |
16 | $ composer require limenius/react-bundle
17 |
18 | This command requires you to have Composer installed globally, as explained
19 | in the *installation chapter* of the Composer documentation.
20 |
21 | ### Step 2: Enable the Bundle
22 |
23 | Then, enable the bundle by adding the following line in the `app/AppKernel.php`
24 | file of your project:
25 |
26 | ```php
27 | // app/AppKernel.php
28 |
29 | // ...
30 | class AppKernel extends Kernel
31 | {
32 | public function registerBundles()
33 | {
34 | $bundles = array(
35 | // ...
36 |
37 | new Limenius\ReactBundle\LimeniusReactBundle(),
38 | );
39 |
40 | // ...
41 | }
42 |
43 | // ...
44 | }
45 | ```
46 |
47 | ### Step 3: (optional) Configure the bundle
48 |
49 | The bundle comes with a sensible default configuration, which is listed below. If you skip this step, these defaults will be used.
50 | ```yaml
51 | limenius_react:
52 | # Other options are "server_side" and "client_side"
53 | default_rendering: "both"
54 |
55 | serverside_rendering:
56 | # In case of error in server-side rendering, throw exception
57 | fail_loud: false
58 |
59 | # Replay every console.log message produced during server-side rendering
60 | # in the JavaScript console
61 | # Note that if enabled it will throw a (harmless) React warning
62 | trace: false
63 |
64 | # Mode can be `"phpexecjs"` (to execute Js from PHP using PhpExecJs),
65 | # or `"external"` (to rely on an external node.js server)
66 | # Default is `"phpexecjs"`
67 | mode: "phpexecjs"
68 |
69 | # Location of the server bundle, that contains React and React on Rails.
70 | # null will default to `%kernel.root_dir%/Resources/webpack/server-bundle.js`
71 | # Only used with mode `phpexecjs`
72 | server_bundle_path: null
73 |
74 | # Only used with mode `external`
75 | # Location of the socket to communicate with a dummy node.js server.
76 | # Socket type must be acceptable by php function stream_socket_client. Example unix://node.sock, tcp://127.0.0.1:5000
77 | # More info: http://php.net/manual/en/function.stream-socket-client.php
78 | # Example of node server:
79 | # https://github.com/Limenius/symfony-react-sandbox/blob/master/external-server.js
80 | # null will default to `unix://%kernel.project_dir%/var/node.sock`
81 | server_socket_path: null
82 |
83 | cache:
84 | enabled: false
85 | # name of your app, it is the key of the cache where the snapshot will be stored.
86 | key: "app"
87 | ```
88 |
89 | ## JavaScript and Webpack Set Up
90 |
91 | In order to use React components you need to register them in your JavaScript. This bundle makes use of the React On Rails npm package to render React Components (don't worry, you don't need to write any Ruby code! ;) ).
92 |
93 | ```bash
94 | npm install react-on-rails
95 | ```
96 |
97 | Your code exposing a react component would look like this:
98 |
99 | ```js
100 | import ReactOnRails from 'react-on-rails';
101 | import RecipesApp from './RecipesAppServer';
102 |
103 | ReactOnRails.register({ RecipesApp });
104 | ```
105 |
106 | Where RecipesApp is the component we want to register in this example.
107 |
108 | Note that it is very likely that you will need separated entry points for your server-side and client-side components, for dealing with things like routing. This is a common issue with any universal (isomorphic) application. Again, see the sandbox for an example of how to deal with this.
109 |
110 | If you use server-side rendering, you are also expected to have a Webpack bundle for it, containing React, React on Rails and your JavaScript code that will be used to evaluate your component.
111 |
112 | Take a look at [the webpack configuration in the symfony-react-sandbox](https://github.com/Limenius/symfony-react-sandbox/blob/master/webpack.config.serverside.js) for more information.
113 |
114 | If not configured otherwise this bundle will try to find your server side JavaScript bundle in `app/Resources/webpack/server-bundle.js`
115 |
116 | ## Start using the bundle
117 |
118 | You can insert React components in your Twig templates with:
119 |
120 | ```twig
121 | {{ react_component('RecipesApp', {'props': props}) }}
122 | ```
123 |
124 | Where `RecipesApp` is, in this case, the name of our component, and `props` are the props for your component. Props can either be a JSON encoded string or an array.
125 |
126 | For instance, a controller action that will produce a valid props could be:
127 |
128 | ```php
129 | /**
130 | * @Route("/recipes", name="recipes")
131 | */
132 | public function homeAction(Request $request)
133 | {
134 | $serializer = $this->get('serializer');
135 | return $this->render('recipe/home.html.twig', [
136 | 'props' => $serializer->serialize(
137 | ['recipes' => $this->get('recipe.manager')->findAll()->recipes], 'json')
138 | ]);
139 | }
140 | ```
141 |
142 | ## Server-side, client-side or both?
143 |
144 | You can choose whether your React components will be rendered only client-side, only server-side or both, either in the configuration as stated above or per twig tag basis.
145 |
146 | If you set the option `rendering` of the twig call, you can override your config (default is to render both server-side and client-side).
147 |
148 | ```twig
149 | {{ react_component('RecipesApp', {'props': props, 'rendering': 'client_side'}) }}
150 | ```
151 |
152 | Will render the component only client-side, whereas the following code
153 |
154 | ```twig
155 | {{ react_component('RecipesApp', {'props': props, 'rendering': 'server_side'}) }}
156 | ```
157 |
158 | ... will render the component only server-side (and as a result the dynamic components won't work).
159 |
160 | Or both (default):
161 |
162 | ```twig
163 | {{ react_component('RecipesApp', {'props': props, 'rendering': 'both'}) }}
164 | ```
165 |
166 | You can explore these options by looking at the generated HTML code.
167 |
168 | ## Debugging
169 |
170 | One important point when running server-side JavaScript code from PHP is the management of debug messages thrown by `console.log`. ReactBundle, inspired React on Rails, has means to replay `console.log` messages into the JavaScript console of your browser.
171 |
172 | To enable tracing, you can set a config parameter, as stated above, or you can set it in your template in this way:
173 |
174 | ```twig
175 | {{ react_component('RecipesApp', {'props': props, 'trace': true}) }}
176 | ```
177 |
178 | Note that in this case you will probably see a React warning like
179 |
180 | *"Warning: render(): Target node has markup rendered by React, but there are unrelated nodes as well. This is most commonly caused by white-space inserted around server-rendered markup."*
181 |
182 | This warning is harmlesss and will go away when you disable trace in production. It means that when rendering the component client-side and comparing with the server-side equivalent, React has found extra characters. Those characters are your debug messages, so don't worry about it.
183 |
184 | ## Server-Side modes
185 |
186 | This bundle supports two modes of using server-side rendering:
187 |
188 | * Using [PhpExecJs](https://github.com/nacmartin/phpexecjs) to auto-detect a JavaScript environment (call node.js via terminal command or use V8Js PHP) and run JavaScript code through it. This is more friendly for development, as every time you change your code it will have effect immediatly, but it is also more slow, because for every request the server bundle containing React must be copied either to a file (if your runtime is node.js) or via memcpy (if you have the V8Js PHP extension enabled) and re-interpreted. It is more **suited for development**, or in environments where you can cache everything.
189 |
190 | * Using an external node.js server ([Example](https://github.com/Limenius/symfony-react-sandbox/blob/master/external-server.js)). It will use a dummy server, that knows nothing about your logic to render React for you. This is faster but introduces more operational complexity (you have to keep the node server running). For this reason it is more **suited for production**.
191 |
192 | ## Redux
193 |
194 | If you're using [Redux](http://redux.js.org/) you could use the bundle to hydrate your store's:
195 |
196 | Use `redux_store` in your twig file before you render your components depending on your store:
197 |
198 | ```twig
199 | {{ redux_store('MySharedReduxStore', initialState ) }}
200 | {{ react_component('RecipesApp') }}
201 | ```
202 | `MySharedReduxStore` here is the identifier you're using in your javascript to get the store. The `initialState` can either be a JSON encoded string or an array.
203 |
204 | Then, expose your store in your bundle, just like your exposed your components:
205 |
206 | ```js
207 | import ReactOnRails from 'react-on-rails';
208 | import RecipesApp from './RecipesAppServer';
209 | import configureStore from './store/configureStore';
210 |
211 | ReactOnRails.registerStore({ configureStore });
212 | ReactOnRails.register({ RecipesApp });
213 | ```
214 |
215 | Finally use `ReactOnRails.getStore` where you would have used the object you passed into `registerStore`.
216 |
217 | ```js
218 | // Get hydrated store
219 | const store = ReactOnRails.getStore('MySharedReduxStore');
220 |
221 | return (
222 |
223 |
224 |
225 | );
226 | ```
227 |
228 | Make sure you use the same identifier here (`MySharedReduxStore`) as you used in your twig file to set up the store.
229 |
230 | You have an example in the [Sandbox](https://github.com/Limenius/symfony-react-sandbox).
231 |
232 | ## Using asset versioning
233 |
234 | If you are using [webpack encore](https://github.com/symfony/webpack-encore) you may be using assets versioning using a [json manifest file](https://symfony.com/blog/new-in-symfony-3-3-manifest-based-asset-versioning).
235 | In this case, having to change your configuration is very bothersome and should be done automatically using your `manifest.json` file. This is how to do it:
236 |
237 | ### Create a custom renderer
238 |
239 | ```php
240 | serverBundlePath .= $packages->getUrl($serverBundlePath);
256 | }
257 | }
258 | ```
259 |
260 | ### Update your services configuration to override the default service
261 |
262 | ```yaml
263 | services:
264 | limenius_react.react_renderer:
265 | class: App\Renderer\CustomPhpExecJsReactRenderer
266 | arguments:
267 | - '%kernel.project_dir%/public' # here you set the base path
268 | - '%limenius_react.fail_loud%'
269 | - '@limenius_react.context_provider'
270 | - '@logger'
271 | calls:
272 | - [setPackage, ['@assets.packages', 'build/js/server-bundle.js']]
273 | ```
274 |
275 | Some things to keep in mind:
276 |
277 | - the value `build/js/server-bundle.js` is the same path you would use for an assets render in twig
278 | - the `server_bundle_path` configuration becomes useless after this manipulation
279 | - this does not consider the behavior with a node server rendering
280 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "limenius/react-bundle",
3 | "description": "Client and Server-side react rendering in a Symfony Bundle",
4 | "type": "symfony-bundle",
5 | "keywords": ["react", "isomorphic"],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "nacho",
10 | "email": "nacho@limenius.com"
11 | }
12 | ],
13 | "autoload": {
14 | "psr-4": { "Limenius\\ReactBundle\\": "" }
15 | },
16 | "require": {
17 | "php": ">=5.5.0",
18 | "symfony/config": "^2.7.0|^3.0.6|^4.0|^5.0|^6.0",
19 | "symfony/http-kernel": "^2.7.0|^3.0.6|^4.0|^5.0|^6.0",
20 | "symfony/dependency-injection": "^2.7.0|^3.0.6|^4.0|^5.0|^6.0",
21 | "limenius/react-renderer": "^5.0.0"
22 | },
23 | "require-dev": {
24 | "squizlabs/php_codesniffer": "^2.5",
25 | "escapestudios/symfony2-coding-standard": "^2.9",
26 | "wimg/php-compatibility": "^7.0"
27 | },
28 | "scripts": {
29 | "default-scripts": [
30 | "rm -rf vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/PHPCompatibility; cp -rp vendor/wimg/php-compatibility vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/PHPCompatibility"
31 | ],
32 | "post-install-cmd": [
33 | "@default-scripts"
34 | ],
35 | "post-update-cmd": [
36 | "@default-scripts"
37 | ]
38 | },
39 | "minimum-stability": "dev"
40 | }
41 |
--------------------------------------------------------------------------------
/ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ReactBundle coding standard.
4 |
5 | vendor/
6 | Resources/
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 0
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ./
23 |
24 |
--------------------------------------------------------------------------------