├── .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 | [![Latest Stable Version](https://poser.pugx.org/limenius/react-bundle/v/stable)](https://packagist.org/packages/limenius/react-bundle) 16 | [![Latest Unstable Version](https://poser.pugx.org/limenius/react-bundle/v/unstable)](https://packagist.org/packages/limenius/react-bundle) 17 | [![License](https://poser.pugx.org/limenius/react-bundle/license)](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 | --------------------------------------------------------------------------------