Embedding a create-react-app project into A CakePHP view

Written by James McDonald

August 21, 2020

Step 1

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 /path/to/cakephp/app
create-react-app pick-app

Step 2

Next edit your pick-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 react files

{
"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build && rm -rf /var/www/html/wms/app/webroot/pick-app && cp -rv build /var/www/html/wms/app/webroot/pick-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 or yarn build

The asset-manifest.json file

When you do a production build of your react project it includes a asset-manifest.json in the build folder:

{
  "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"
}

Step 3

Next you need to modify your CakePHP view file to include the production build react files. To get it working you need to modify the controller action and the corresponding view.

The CakePHP controller code opens and reads the asset-manifest.json and then sends the relevant resources that need to be loaded in the CakePHP to the view as variables

I am going to embed my react project into a CakePHP view located at ‘http://localhost:8081/wms/Shipments/pickStock’ the pickStock action is located in the Controller/ShipmentsController.php and the view file is View/Shipments/pick_stock.ctp file

Controller/ShipmentsController.php

public function pickStock() {

		$file = new File(WWW_ROOT . '/pick-app/asset-manifest.json');
        $manifest = json_decode($file->read());
        $file->close();
    
        $js = [];
        $css = [];
        foreach ($manifest as $key => $value) {
            if (preg_match('/\.js$/', $key ) === 1) {
                    $js[] = $value;
            }
            if ( preg_match('/\.css$/', $key ) === 1) {
                    $css[] = $value;
            }
        }
   
        $maincss = 'main.css';
        $mainjs = 'main.js';
        $this->set('maincss', $manifest->$maincss);
        $this->set('mainjs', $manifest->$mainjs);
        $this->set(compact('js', 'css'));
	}

View/Layout/default.ctp

Edit your CakePHP Layout so you put your normal site CSS into the ‘css’ block and then from the react embedded view put it in the ‘cssFromView’ block so it will appear after your general site CSS.

 <?php $this->Html->css(
		    [
		        'bootstrap.min',
		        'bootstrap-theme.min',
		        'droid_sans_font',
		        'jmits',
		    ], [
		        'block' => 'css',
		    ]
		);
		echo $this->fetch('css');
		echo $this->fetch('cssFromView');
		?>

View/Shipment/pick_stock.ctp

<?php foreach( $css as $style ): ?>

<?php $this->Html->css('/pick-app' . $style, [
	'block' => 'cssFromView'
        // I add a $this->fetch('cssFromView') 
        // block in my View/Layout/default.ctp file
        // _after_ a call to the default CSS block
        // to make sure I can dictate the ordering
        // of the react CSS
]); ?>

<?php endforeach; ?>
<!-- the baseUrl parameter is so I can dynamically 
     assign the URL that react connects to for fetch calls 
     against the CakePHP API.
     In the react development environment 
     I put the API URL in the public/index.html so 
     it works in both dev and prod
-->
<div baseUrl="<?= Router::url('/', true); ?>" id="root"></div>

<!-- put the script file after the root dom element -->
<?php foreach( $js as $script ): ?>
<?= $this->Html->script('/pick-app' . $script); ?>
<?php endforeach;?>

Step 4

Modify any CakePHP actions that you want to call from react to make sure they allow access from your React development environment URL which is usually http://localhost:3000 also handle the pre-flight options check with the appropriate headers. 

This is an example of an action is the Controller/LabelsController.php that has been modified to accept a request from a react http://localhost:3000 development instance and will also work when the react page is mounted in the CakePHP view. (Yes i know the code is nasty)

You can also use your default actions without creating view files by using JSON Views and telling the action which variables to encode as JSON using $this->set(‘_serialize’, [ ‘viewVar1′, viewVar2’]); 

 public function multiEdit($id = null)
    {
		$this->layout = 'ajax';
		$this->render(false);

		//$this->log($this->request->data);
        /*
        if (!$this->Label->exists($id)) {
        throw new NotFoundException(__('Invalid label'));
        }*/

        if ($this->request->is('options')) {

            $this->response->header('Access-Control-Allow-Origin', 'http://localhost:3000');
            $this->response->header('Access-Control-Allow-Credentials', "true");
            $this->response->header("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT");
            $this->response->header("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers");
            return;
        }
        if ($this->request->is(['post', 'put'])) {
            $this->response->header('Access-Control-Allow-Origin', 'http://localhost:3000');
            if ($this->Label->saveMany($this->request->data)) {
                // $this->log($this->request->data);

                if ($this->request->is('ajax')) {
       
                    $this->autoRender = false;
                    $this->response->type = 'json';
                    $msg = [
                        'result' => 'success',
                        'message' => 'Successfully updated label',
                    ];
                    $this->response->body(json_encode($msg));

                    return;
                }
                $this->Flash->set(__('The label has been saved.'));
                return $this->redirect(['action' => 'index']);
            } else {
                $this->autoRender = false;
                $this->response->type = 'json';
                $msg = [
                    'result' => 'danger',
                    'message' => 'The data could not be saved',
                ];
                $this->response->body(json_encode($msg));

            }

		}

    }

Some notes about the above code

In order for CakePHP to recognize the request as “AJAX” when using the $this->request->is(‘ajax’)  you need to add a “X-Requested-With”: “XMLHttpRequest” header to your fetch request in the client.

When you make a POST request with non-default POST data (in this case I’m sending JSON and not the more usual x-www-form-urlencoded)  the Google Chrome client will do an OPTIONS pre-flight check so you need to respond and tell the client what the server will allow.

Making the API URL dynamic when connecting from both React development and React embedded in CakePHP environments

In CakePHP in the view file specify the baseURL to the CakePHP instance

<div baseUrl="<?= Router::url('/', true); ?>" id="root"></div>

In the react development public/index.html file specify the the CakePHP  URL

<div baseUrl=”http://localhost:8081/wms” id=”root”></div>

Once you have the above configured then you can query that URL in the React projects index.js and pass it to your top level react component or use anywhere it’s needed

//src/index.js in react project
const root = document.getElementById('root');

const baseUrl = root.getAttribute('baseUrl');

ReactDOM.render(<App baseUrl={baseUrl} />, root );

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

You May Also Like…

How to Research a CPU Upgrade

How to Research a CPU Upgrade

Upgrade Time! Doing a lot of VMWare Workstation virtualization to create labs for self-study and training. Finding...