CakeDC Blog

TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP

File uploading, file storage and CakePHPs MediaView class

This article includes how to upload and store files, because I've seen a lot of discussion about that too, but if you're just interested in how to use the MediaView class scroll down.

Handling file uploads in CakePHP

First let's start with the required form, to create a file upload form all you have to do is this:

echo $form->create('Media', array('action' => 'upload', 'type' => 'file'));
echo $form->file('file');
echo $form->submit(__('Upload', true));

 

The "type" in the options of Form::create() takes post, get or file. To configure the form for file uploading it has to be set to file which will render the form as a multipart/form-data form.

When you submit the form now, you'll get data like this in $this->data of your controller:

Array
(
[Media] => Array
(
[file] => Array
(
[name] => cake.jpg
[type] => image/jpeg
[tmp_name] => /tmp/hp1083.tmp
[error] => 0
[size] => 24530
)
)
)

Ok, now the big question with a simple answer is where the file data should be processed, guess where. Right – in the model because it's data to deal with and validation to do against it. Because it's a recurring task to upload files I suggest you to write a behaviour for it or convert your existing component to a behaviour.

If you keep it generic you can extend it with a CsvUpload, VideoUpload or ImageUpload behaviour to process the file directly after its upload or do special stuff with it, like resizing the image or parsing the csv file and store its data in a (associated) model.

We're not going to show you our own code here for obvious reasons, but I'll give you a few hints what you can or should do inside of the behavior:

  1. Validate the uploaded field, the field itself contains already an error code if something was wrong with the upload. Here is a link to the php manual page that shows you the list of the errors that you can get from the form data. http://www.php.net/manual/en/features.file-upload.errors.php
  2. Validate the uploaded file, is it really the kind of file you want and does it really contain the data structure you want?
  3. Check if the target destination of the file is writeable, create directories, whatever is needed and error handling for it, I suggest you to use CakePHP's File and Folder classes for that.
  4. Add a callback like beforeFileSave() and afterFileSave() to allow possible extending behaviors to use them.

Database vs file system storage

Feel free to skip that part if you already store the files in the file system.

Storing files in the database is in nearly all cases a bad solution because when you get the file it has to go its way through the database connection, which can, specially on servers that are not in the same network, cause performance problems.

Advantages of storage in the file system:

  1. Easy and direct file access, to parse them (csv, xml...) or manipulate them (images)
  2. You don't need to install any additional software to manage them
  3. Easy to move and mount on other machines
  4. Smaller then stored in a DB

The suggested solution is to store meta data of the file like size, hash, maybe path and other related info in a DB table and save the file in the file system.

Some people come up with the security and want to store a file because of that in the database which is wrong. You should not store the file in a public accessible directory like the webroot of the application. Store it in another location like APP/media. You control the access to the file by checking the permissions against the DB records of your meta data and sending it by using the CakePHP MediaView class, I'll explain later how to use it.

I don't say that storage of files inside the DB is in general a bad idea but for web based applications it is in nearly every case a bad idea.

File system Performance

A bottleneck in the long run on every file system is a large amount of files in a single directory. Imagine just 10.000 users and each has an individual avatar image. Further ext3 for example is limited to 32000 sub folders, other file systems have maybe similar restrictions. You can find a list of file system limitations here: http://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits

To avoid performance problems caused by that you should store your files in a pseudo-random directory structure like APP/media/32/a5/3n/. This will also allow you to easily mount some of the semi-random created directories on another machine in the case you run out of disk space.

/**
 * Builds a semi random path based on the id to avoid having thousands of files
 * or directories in one directory. This would result in a slowdown on most file systems.
 *
 * Works up to 5 level deep
 *
 * @see http://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits
 * @param mixed $string
 * @param integer $level
 * @return mixed
 * @access protected
 */
	protected function _randomPath($string, $level = 3) {
		if (!$string) {
			throw new Exception(__('First argument is not a string!', true));
		}

		$string = crc32($string);
		$decrement = 0;
		$path = null;
		
		for ($i = 0; $i < $level; $i++) {
			$decrement = $decrement -2;
			$path .= sprintf("%02d" . DS, substr('000000' . $string, $decrement, 2));
		}

		return $path;
	}

You should also know that php running in safe mode does not allow you to create more then one directory deep in one call. You have to take this in consideration, the above function does not cover that because safe mode is basically deprecated and will be also removed in php6

Sending a file to the client – or the unknown MediaView class

From what I've seen in the ruins of outsourced projects that asked us for rescue and also in the CakePHP googlegroup I think not many people are aware that CakePHP has a view that is thought to be used for downloads and display (images, text...) of files. It's called the MediaView class.

http://api.cakephp.org/class/media-view

I'll now explain you how to use this class to send files to the client.

/**
 * Sends a file to the client
 *
 * @param string $id UUID
 * @access public
 */
	public function download($id = null) {
		$this->Media->recursive = -1;
		$media = $this->Media->read(null, $id);

		if (empty($media)) {
		$this->redirect('/', 404, true);
		}
		
		$this->set('cache', '3 days');
		$this->set('download', true);
		$this->set('name', $media['Media']['slug']);
		$this->set('id', $media['Media']['filename']);
		$this->set('path', APP . 'media' . DS . $media['Media']['path']);
		$this->set('modified', $media['Media']['modified']);
		$this->set('mimeType', $media['Media']['mime_type']);
		$this->set('extension', $media['Media']['extension']);

		$this->view = 'Media';
		$this->autoLayout = false;
		if ($this->render() !== false) {
			$this->Media->updateAll(
				array('Media.downloads' => 'Media.downloads + 1'),
				array('Media.id' => $id));
		}
	}

You simply have to set autoLayout to false and the view class to media.

$this->view = 'Media';
$this->autoLayout = false;

There are a few view variables to set to “configure” the file download or display. To control if you want to make the client downloading the file or to display it, in the case of images for example, you simply set 'download' to true or false;

	$this->set('download', true);

You can control the browser caching of the file by setting cache. Please not that you do not have to use caching if download is set to true! Downloads do not need caching.

	$this->set('cache', '3 days');

The next part might be a little confusing, you have “id” and “name”. Id is the actual file on your server you want to send while name is the filename under which you want to send the file to the client. “path” is the path to the file on the server.

	$this->set('name', $media['Media']['slug']);
$this->set('id', $media['Media']['filename']);
$this->set('path', APP . 'media' . DS . $media['Media']['path']);

If you want to send a mime type that does not already in the MediaView class you can set it.

	$this->set('mimeType', $media['Media']['mime_type']);

If you don't set it, the class will try to determine the mime type by the extension.

	$this->set('extension', $media['Media']['extension']);

Note that you have to set the extension to make it work and that the extension is attached to the filename! If you store the filename with an extension you have to break it up.

When everything is set you can check if render() was successfully and do whatever you want after that, for example count the download.

	if ($this->render() !== false) {
	$this->Media->updateAll(
	array('Media.downloads' => 'Media.downloads + 1'),
	array('Media.id' => $id));
}

Closing words

I hope you enjoyed reading the article and it helped you improving your knowledge about CakePHP. Feel free to ask further questions by using the comment functionality. Have fun coding!

Latest articles

CakePHP API Plugin

Are you creating an API in CakePHP? This task looks very popular these days, and most of our clients need an API to expose certain services to their own rich client applications, or third party services. Even if it's easy to configure CakePHP to expose a REST API, and there are other plugins that could help you building an API, we found ourselves working on specific tweaks per project to adjust the way the API was designed, so we decided to wrap all these ideas and create a specific CakePHP API Plugin including

  • Services definition
  • Integrated CRUD
  • Nested resources
  • Pagination
  • Sorting
  • Associations
  • Versioning
  • Custom Extensions (data format / transformers)
  • Self documentation
We've gathered all the best practices around API building and CakePHP and wrapped them into an easy to install and setup Plugin to be used as the foundation of your API intensive CakePHP projects. Let's walkthru some of the Plugin features using an example application: the bookmarker tutorial http://book.cakephp.org/3.0/en/tutorials-and-examples/bookmarks/intro.html We'll assume you've already created a new CakePHP application and configured it to use the bookmarker database (schema dump here http://book.cakephp.org/3.0/en/tutorials-and-examples/bookmarks/intro.html#creating-the-database).

Setting up the CakePHP API Plugin

Download the plugin first composer require cakedc/cakephp-api:dev-master Then ensure plugin is loaded in you bootstrap.php file Plugin::load('CakeDC/Api', ['bootstrap' => true, 'routes' => true]);

Now you have an API!

Test your newly configured "default" API using curl curl -X GET http://bookmarker.dev/api/bookmarks You'll get something similar to: { "status": "success", "data": [], "pagination": { "page": 1, "limit": 20, "pages": 0, "count": 0 }, "links": [ { "name": "self", "href": "http:\/\/bookmarker.dev\/api\/bookmarks", "rel": "\/api\/bookmarks", "method": "GET" }, { "name": "bookmarks:add", "href": "http:\/\/bookmarker.dev\/api\/bookmarks", "rel": "\/api\/bookmarks", "method": "POST" } ] } If you look at the provided output you'll identify we've used a JSend default renderer (status, data) and we append some extra data under 'links' (HATEOAS dynamically generated for your CRUDs) and pagination. The specific "extensions" used can be configured and custom extensions created for your specific needs, see https://github.com/CakeDC/cakephp-api/blob/master/docs/Documentation/extensions.md We'll publish a couple tutorials soon covering some of the features implemented, and explaining how did we use the CakePHP API Plugin to address specific use cases. Meanwhile, please check the documentation here https://github.com/CakeDC/cakephp-api/blob/master/docs/Documentation/overview.md

Giving back to the community

This Plugin's development has been sponsored by the Cake Development Corporation. Contact us if you are interested in:  

Create Google app for web oauth2 login step by step

Here's a step by step tutorial about how to create a web oauth2 app in Google dashboard.

Google app oauth login app 1
  • Add some cool name for your new Google app project and click "Create"
Google app oauth login app 2
  • Under "Library" section, create a new Google+ API project
Google app oauth login app 3
  • Click "Enable" in the dashboard tab
Google app oauth login app 4
  • Under "Credentials" menu, click "Oauth consent screen" tab and enter some cool name to be displayed to users when requesting their access to your application. Then click "Save".
Google app oauth login app 5
  • Under "Credentials" menu, click "Create credentials" and select "Oauth client ID".
Google app oauth login app 6
  • Now click "Web application" radio, and type your domain name and oauth callback
    • Under "Authorized Javascript origins", add your domain name: mydomain.com
    • Under "Authorized redirect URIs", add all the allowed callback url's to your application. For example if you are using CakeDC/Users Plugin, you'll need to add mydomain.com/auth/google
  • Then click "Save"
Google app oauth login app 7
  • Copy the Iauth client and secret id's into your application configuration
  • Be careful, some browsers will append blank spaces to the codes, remove any extra blank space (trim)
Google app oauth login app 8
  • Ensure the API is enabled, you can test your application now and check there is "Traffic" displayed
  You have now a Google app configured to provide Oauth2 login to your web application. Enjoy!                  

Login with Google Oauth2 in CakePHP using CakeDC/Users Plugin

This article is inspired by this question in Stack Overflow and belongs to a series of articles describing the step by step tutorial to configure CakeDC Users Plugin with the most commonly used Oauth2 providers, in this case we'll configure Google login. We'll assume you have a working CakePHP application with no Auth configured yet.

Setup

Use composer to install the CakeDC Users Plugin and the required oauth2 providers To be able to configure the callbacks in Google dashboard, you'll need to create a virtual host for you application. You don't need a working domain name, you could use something like "mydomain.dev" but Google requires a domain name (no localhost). composer require cakedc/users:@stable composer require league/oauth2-google:@stable Load it from your bootstrap.php file Plugin::load('CakeDC/Users', ['routes' => true, 'bootstrap' => true]); Run migrations to add 2 new tables: 'users' and 'social_accounts' bin/cake migrations migrate -p CakeDC/Users

Configuration

Load the Component in your src/Controller/AppController.php public function initialize() { parent::initialize(); // // ... // $this->loadComponent('CakeDC/Users.UsersAuth'); }

Create a new Google application

<?php // /config/users.php file contents $config = [ 'Users.Social.login' => true, 'OAuth.providers.google.options.clientId' => 'CLIENT_ID_HERE', 'OAuth.providers.google.options.clientSecret' => 'SECRET_HERE', ]; return $config;
  • Modify your bootstrap.php file to ensure the config file is loaded this way
Configure::write('Users.config', ['users']); //add this line before Plugin::load('CakeDC/Users... Plugin::load('CakeDC/Users', ['routes' => true, 'bootstrap' => true]); This file will override any configuration key present in the Plugin, you can check the configuration options here Configuration. Now you are ready to go to your login page and click "Sign up with Google". Upon successful login, a new user will be created in your users table and related oauth2 tokens will be saved in the social_accounts table. The new user created will have the "user" role (by default, but customizable). And based on your Auth rules, this user will be able to access your site. You are done!

Read more about CakeDC Users Plugin

Giving back to the community

This Plugin's development has been sponsored by the Cake Development Corporation. Contact us if you are interested in: We hope you've enjoyed this short tutorial covering the Google login, stay tunned for new CakePHP + Users Plugin tutorials coming soon...

BOOK A 15 MINUTES FREE
CONSULTING WITH US:
We Bake with CakePHP