Updated: 17 May 2022
Step 1 - Create the React App
I find it good to create the React project inside my CakePHP APP directory so I can keep it under the same git repository. To create the react project use create-react-app:
cd /var/www/wms/src
mkdir React
npx create-react-app edi-app
# My CakePHP project is in /var/www/wms and the react app is as follows
# /var/www/wms/src/React/edi-app
Remember to add node_modules
to .gitignore
, if it isn't there already so you don't bloat your repo
https://reactjs.org/docs/create-a-new-react-app.html
Step 2 - Customize scripts build
Next edit your edi-app/package.json
file to create a custom build entry. The build entry does 3 things:
- Compiles a production build putting it in the default build directory in the react project,
- removes the currently deployed react directory from your CakePHP WWW_ROOT directory and
- copies over the latest production build react files to your CakePHP 4 webroot
package.json
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build && rm -rf ../../../webroot/edi-app && cp -rv build ../../../webroot/edi-app",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
}
Each time you are happy with your react project copy it across to the CakePHP webroot using npm run build
In the above example the files in /var/www/wms/src/React/edi-app/build
are copied to /var/www/wms/webroot/edi-app
This is the contents of /var/www/wms/webroot/edi-app
:
./manifest.json
./static
./static/js
./static/js/main.3df63a03.js.LICENSE.txt
./static/js/main.3df63a03.js.map
./static/js/main.3df63a03.js
./static/css
./static/css/main.ccfeb934.css.map
./static/css/main.ccfeb934.css
./asset-manifest.json
./index.html
./favicon.ico
The asset-manifest.json file
When you do a production build of your react project it includes a asset-manifest.json
{
"main.css": "/static/css/main.81a20791.chunk.css",
"main.js": "/static/js/main.821121cc.chunk.js",
"main.js.map": "/static/js/main.821121cc.chunk.js.map",
"static/js/1.b3836c5f.chunk.js": "/static/js/1.b3836c5f.chunk.js",
"static/js/1.b3836c5f.chunk.js.map": "/static/js/1.b3836c5f.chunk.js.map",
"runtime~main.js": "/static/js/runtime~main.229c360f.js",
"runtime~main.js.map": "/static/js/runtime~main.229c360f.js.map",
"static/css/main.81a20791.chunk.css.map": "/static/css/main.81a20791.chunk.css.map",
"index.html": "/index.html",
"precache-manifest.00bfcb3f81f91832033cf3827e46d489.js": "/precache-manifest.00bfcb3f81f91832033cf3827e46d489.js",
"service-worker.js": "/service-worker.js"
}
Newer versions of create-react-app
have a different layout for the asset-manifest.json
file:
{
"files": {
"main.css": "/static/css/main.ccfeb934.css",
"main.js": "/static/js/main.3df63a03.js",
"index.html": "/index.html",
"main.ccfeb934.css.map": "/static/css/main.ccfeb934.css.map",
"main.3df63a03.js.map": "/static/js/main.3df63a03.js.map"
},
"entrypoints": [
"static/css/main.ccfeb934.css",
"static/js/main.3df63a03.js"
]
}
Step 3 - Implement a ReactEmbed Component to Read asset-manifest.json
To allow the Controller Action to open and read the asset-manifest.json
we can use a custom component:
ReactEmbedComponent
<?php
// src/Controller/Component/ReactEmbedComponent.php
declare(strict_types=1);
namespace App\Controller\Component;
use Cake\Collection\Collection;
use Cake\Controller\Component;
use Cake\Http\Exception\NotFoundException;
class ReactEmbedComponent extends Component
{
protected $controller = null;
public function initialize(array $config): void
{
parent::initialize($config);
}
/**
* @param string $subFolder Subfolder webroot/shipment-app <= shipment-app is subfolder
*/
public function getAssets($subFolder)
{
$manifest = $this->assets($subFolder);
return $this->parseManifest($manifest, $subFolder);
}
/**
* @param string $subFolder Folder that the react app is in
* e.g. subFolder is webroot/pick-app
*
* @return object
* @throws NotFoundException
*/
public function assets($subFolder): array|null
{
$assetManifest = WWW_ROOT . $subFolder . DS . 'asset-manifest.json';
$file = @file_get_contents($assetManifest);
if ($file === false) {
throw new NotFoundException($assetManifest . ' missing');
}
$manifest = json_decode($file, true);
if (array_key_exists('entrypoints', $manifest)) {
// just entry points
$manifest = $manifest['entrypoints'];
}
return $manifest;
}
protected function parseManifest($manifest, string $subFolder): array
{
$manifest = new Collection($manifest);
$manifest = $manifest->map(function ($asset) use ($subFolder) {
return DS . $subFolder . DS . $asset;
})->groupBy(function ($asset) {
$parts = explode('.', $asset);
// get the .css or .js part for grouping
$extension = end($parts);
return $extension;
})->toArray();
// not sure if this can happen but just in case either expected key is missing
$manifest = $manifest + [
'js' => [],
'css' => []
];
return $manifest;
}
}
Step 4 - Edit Controller/ShipmentsController.php
In the Controller load the ReactEmbedComponent we will use it to read asset-manifest.json
and return an assets array
// src/Controller/ShipmentsController.php
class ShipmentsController extends AppController
{
public function initialize(): void
{
parent::initialize();
$this->loadComponent('ReactEmbed');
}
/**
* Edit method
*
* @param string|null $id Shipment id.
* @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function edit($id = null)
{
$assets = $this->ReactEmbed->getAssets(
'edi-app'
);
$baseUrl = $this->request->getAttribute('webroot');
$productTypes = $this->Shipments->ProductTypes->find('list', ['conditions' => ['active' => 1]]);
$this->set(compact('assets', 'baseUrl', 'productTypes'));
}
Example of assets array generated by ReactEmbedComponent
Step 5 - Modify templates/layout/default.php
Edit your CakePHP 4 layout. Normally when using the CSS helper all the CSS links go into the default 'css' block. Putting a fetch statement in for 'react-css
' block will allow you to embed the CSS after your general site CSS and make sure your React styles will be after and therefore over-ride and take precedence over the site CSS.
<head>
<?= $this->Html->charset() ?>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title><?= h($this->fetch('title')) ?></title>
<?= $this->fetch('meta') ?>
<?= $this->fetch('css') ?>
<?= $this->Html->css('main'); ?>
<?= $this->fetch('cust-css'); ?>
<?= $this->fetch('react-css'); ?>
<?= $this->Html->meta(['name' => "google", 'content' => "notranslate"]); ?>
</head>
Notice the /wms/edi-app/static/css/main.ccfeb934.css
file is after the other site CSS
Do the same thing for the JavaScript assets. Modify the default layout to have a custom script block. In this example I called it 'from_view'
<?php
echo $this->fetch('tb_body_start');
echo $this->fetch('tb_flash');
echo $this->fetch('content');
echo $this->fetch('tb_footer');
echo $this->fetch('script');
echo $this->fetch('postLink');
echo $this->fetch('from_view');
echo $this->fetch('tb_body_end');
?>
</html>
Step 6 - Add a root node and react configuration to templates/Shipments/edit.php
Here we have the view template where we assign the react js and css assets to their respective blocks and provide some vital configuration for the react app to function.
- Note the csrfToken which the react app needs to send back to CakePHP 4 for CakePHP 4 CSRF Middleware to trust what react sends to it.
- a
<div id="root" data-baseurl="/wms/"></div>
is where the react app will load and the data-baseurl attribute will allow the react app to know where to post back to.
<?php echo $this->Html->scriptBlock(sprintf(
'var csrfToken = %s;',
json_encode($this->request->getAttribute('csrfToken'))
)); ?>
<?php foreach ($assets['css'] as $style) : ?>
<?php $this->Html->css($style, [
'block' => 'react-css',
]); ?>
<?php endforeach; ?>
<?php foreach ($assets['js'] as $script) : ?>
<?= $this->Html->script($script, ['block' => 'from_view']); ?>
<?php endforeach; ?>
<?= $this->Html->tag('div', null, [
'data-baseurl' => $baseUrl,
'id' => 'root',
]); ?>
Step 7 - Implement CORS Middleware
Implement CORS Middleware in CakePHP 4 to allow calls to CakePHP 4 from your React development environment URL which is usually http://localhost:3000 also handle the pre-flight options check with the appropriate headers.
https://github.com/toggenation/cakephp-cors
Step 8 - Make the CakePHP 4 Backend API URL available to your React Dev Environment
In the CRA (Create React App) project specify the the CakePHP URL in index.js
// src/React/edi-app/src/index.js
import React from "react";
import ReactDOM from "react-dom";
import Root from "./Components/Root";
import 'bootstrap/dist/css/bootstrap.min.css';
const root = document.getElementById("root");
if (process.env.NODE_ENV === 'development') {
root.setAttribute('data-baseurl', 'http://10.197.3.73:6002/wms/');
}
const baseUrl = root.getAttribute("data-baseurl");
ReactDOM.render(<Root baseUrl={baseUrl} />, root);
So now when you run npm run start
the dev react app will have your react devel URL available
Hey James, this was super useful. Thanks for sharing.
FYI - if you're using React Router and have different/dynamic paths, to prevent Cake throwing Not Found errors you can add a wildcard to the routes and put it at the end of the scope.
EG:
$builder->connect('/*', ['controller' => 'React', 'action' => 'embed']);
Maybe there's a better way to do it, but it worked for me.
Cheers
Brad
Thanks this is a good idea.