diff --git a/.gitignore b/.gitignore index 3a4edf69..cb4728dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -.project +samples/PhotoAlbumCake/Config/private.php diff --git a/README.md b/README.md index 87999112..f87160df 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ For PHP, Cloudinary provides an extension for simplifying the integration even f  **Take a look at our [Getting started guide for PHP](http://cloudinary.com/documentation/php_integration#getting_started_guide)**. +## CakePHP ## +Special support to the cake PHP is included. You can access the code, installation and usage information [in the `cake_plugin/CloudinaryCake/README.md` file](https://github.com/cloudinary/cloudinary_php/tree/master/cake_plugin/CloudinaryCake) + ## Setup ###################################################################### Download cloudinary_php from [here](https://github.com/cloudinary/cloudinary_php/tarball/master) diff --git a/cake_plugin/CloudinaryCake/Config/Schema/empty b/cake_plugin/CloudinaryCake/Config/Schema/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCake/Config/bootstrap.php b/cake_plugin/CloudinaryCake/Config/bootstrap.php new file mode 100644 index 00000000..6f7f5bbd --- /dev/null +++ b/cake_plugin/CloudinaryCake/Config/bootstrap.php @@ -0,0 +1,26 @@ + array(), + "changeModelDefaults" => true + ); + + public function setup(Model $Model, $settings = array()) { + if (!isset($this->settings[$Model->alias])) { + $this->settings[$Model->alias] = $this->settingsDefaults; + } + $this->settings[$Model->alias] = array_merge( + $this->settings[$Model->alias], (array)$settings + ); + if ($this->settings[$Model->alias]['changeModelDefaults']) { + $this->changeModelDefaults($Model); + } + } + + /// Callbacks /// + public function afterFind(Model $Model, $results, $primary = false) { + $fieldNames = $this->relevantFields($Model); + if (!$fieldNames) { + return $results; + } + + foreach ($results as &$result) { + foreach ($fieldNames as $fieldName) { + $this->updateCloudinaryField($Model, $fieldName, $result); + } + } + return $results; + } + + public function beforeSave(Model $Model, $options = array()) { + foreach ($this->relevantFields($Model, $options) as $fieldName) { + $this->saveCloudinaryField($Model, $fieldName); + } + return true; + } + + public function beforeValidate(Model $Model, $options = array()) { + foreach ($this->relevantFields($Model, $options) as $fieldName) { + $field = @$Model->data[$Model->alias][$fieldName]; + if (is_string($field) && $field) { + if (!(new CloudinaryField($field))->verify()) { + $Model->invalidate($fieldName, "Bad cloudinary signature!"); + return false; + } + } + } + return true; + } + + /// Public Methods /// + public function cloudinaryFields(Model $Model) { + return $this->settings[$Model->alias]['fields']; + } + + /// Private Methods /// + private function createCloudinaryField(Model $Model, $fieldName, $source=NULL) { + $source = $source ? $source : $Model->data; + return new CloudinaryField(isset($source[$Model->alias][$fieldName]) ? + $source[$Model->alias][$fieldName] : ""); + } + + private function updateCloudinaryField(Model $Model, $fieldName, &$data=NULL) { + $source =& $data ? $data : $Model->data; + if (isset($source[$Model->alias][$fieldName]) && $source[$Model->alias][$fieldName] instanceof CloudinaryField) { + return; + } + $source[$Model->alias][$fieldName] = $this->createCloudinaryField($Model, $fieldName, $source); + } + + private function saveCloudinaryField(Model $Model, $fieldName) { + $field = @$Model->data[$Model->alias][$fieldName]; + $ret = NULL; + if ($field instanceof CloudinaryField) { + return; + } elseif (!$field) { + $ret = new CloudinaryField(); + } elseif (is_string($field)) { + $ret = new CloudinaryField($field); + // $ret->verify(); - Validate only in beforeValidate + } elseif (is_array($field) && isset($field['tmp_name'])) { + $ret = new CloudinaryField(); + $ret->upload($field['tmp_name']); + } else { + // TODO - handle file object? + throw new \Exception("Couldn't save cloudinary field '" . $Model->alias . ":" . $fieldName . + "' - unknown input: " . gettype($field)); + } + $Model->data[$Model->alias][$fieldName] = $ret; + } + + private function relevantFields(Model $Model, $options = array()) { + $cloudinaryFields = $this->settings[$Model->alias]['fields']; + if (!(isset($options['fieldList']) && $options['fieldList'])) { + return $cloudinaryFields; + } + return array_intersect($cloudinaryFields, $options['fieldList']); + } + + private static function modifyPropertyUsingForce($instance, $property, $newValue) { + if (version_compare(PHP_VERSION, '5.3.0') >= 0) { + $myClassReflection = new ReflectionClass(get_class($instance)); + $secret = $myClassReflection->getProperty($property); + $secret->setAccessible(true); + $secret->setValue($instance, $newValue); + } + } + + private function changeModelDefaults(Model $Model) { + $schema = $Model->schema(); + foreach ($this->relevantFields($Model) as $fieldName) { + $schema[$fieldName] = new CloneOnAccessArray($schema[$fieldName]); + $schema[$fieldName]['default'] = new CloudinaryField(); + } + self::modifyPropertyUsingForce($Model, "_schema", $schema); + } +} + +class CloneOnAccessArray extends ArrayObject { + public function offsetGet($offset) { + $ret = parent::offsetGet($offset); + return (is_object($ret)) ? clone $ret : $ret; + } +} + +/* +Simplest usage: + Model: + class Photo extends AppModel { + public $actsAs = array('CloudinaryCake.Cloudinary' => array('fields' => array('cloudinaryIdentifier'))); + } + + Controller: + Find: + $photo = $this->Photo->find('first', $options); // returns CloudinaryField in + + Modify: + $photo['Photo']['cloudinaryIdentifier'].upload($new_file); + + Delete: + $photo['Photo']['cloudinaryIdentifier'].delete($new_file); + + Save: + Photo->save($photo); + + Save (from form): + Photo->save($this->request->data); // should work with file upload or identifier + + * Validate identifier upon save + * +*/ diff --git a/cake_plugin/CloudinaryCake/Model/Behavior/empty b/cake_plugin/CloudinaryCake/Model/Behavior/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCake/Model/CloudinaryCakeAppModel.php b/cake_plugin/CloudinaryCake/Model/CloudinaryCakeAppModel.php new file mode 100644 index 00000000..07a039de --- /dev/null +++ b/cake_plugin/CloudinaryCake/Model/CloudinaryCakeAppModel.php @@ -0,0 +1,7 @@ + true, 'routes' => false, + 'path' => ROOT . DS 'vendor' . DS 'cloudinary_php' . DS . 'cake_plugin' . DS . 'CloudinaryCake' . DS)); + + // required when using `private.php` for cloudinary configuration + Configure::load('private'); + \Cloudinary::config(Configure::read('cloudinary')); + +### Composer +1. Create a new directory for myapp + + mkdir myapp + cd myapp + +1. Install CakePHP using composer ([based on CakePHP Cookbook](http://book.cakephp.org/2.0/en/installation/advanced-installation.html#installing-cakephp-with-composer) + 1. Setup Composer and get CakePHP: + + echo '{}' > composer.json + composer config vendor-dir Vendor + composer config repositories.0 pear 'http://pear.cakephp.org' + composer require 'pear-cakephp/cakephp:>=2.4.0' + + 1. Bake a new project + + vendor/bin/cake bake project . + + 1. You may define `CAKE_CORE_INCLUDE_PATH` to a relative path as suggested in the cookbook by adding the following to `webroot/index.php`: + + define( + 'CAKE_CORE_INCLUDE_PATH', + ROOT . DS . APP_DIR . '/Vendor/pear-pear.cakephp.org/CakePHP' + ) + + 1. Add the following lines to `Config/bootstrap.php`: + + // Load composer autoload. + require APP . '/Vendor/autoload.php'; + + // Auto load CloudinaryCake plugin + \CloudinaryCakeLoader::load(); + + +1. Install Cloudinary + + composer require 'cloudinary/cloudinary_php:>=1.0.8' + +1. Configure Cloudinary using the `CLOUDINARY_URL` environment variable, or the `Config/private.php` configuration file + +## Usage + +### CloudinaryBehavior +CloudinaryBehavior adds Cloudinary support for CakePHP Models. It helps storing references to cloudinary images in a simple text field of your model. + +#### Setup +Assuming you have a `Photo` model with `cloudinaryIdentifier` text field for storing cloudinary images references - you can add the following code to your model + +`Models/photo.php`: + + [...] + class Photo extends AppModel { + public $actsAs = array('CloudinaryCake.Cloudinary' => array('fields' => array('cloudinaryIdentifier'))); + [...] + } + +#### Usage +This will allow you to access the `cloudinaryIdentifier` as a CloudinaryField. Here's a sample controller code - + +`Controller/PhotosController.php`: + + class PhotosController extends AppController { + [...] + // set the specified Photo's image to the default one + public function set_default_image($id) { + $options = array('conditions' => array('Photo.' . $this->Photo->primaryKey => $id)); + $photo = $this->Photo->find('first', $options); + + $photo['Photo']['cloudinaryIdentifier']->upload(DEFAULT_IMAGE_PATH); + $this->Photo->save($photo); + } + + [...] + // Creates a new image from post data. Sets $image_url to the cloudinary url of the image with the given transformation. + public function add() { + $this->Photo->create(); + $success = $this->Photo->save($this->request->data); + if ($success) { + $image_url = $this->Photo->data['Photo']['cloudinaryIdentifier']->url(array( + "width" => 100, "height" => 100, "crop" => "fill")); + } + $this->set('photo', $this->Photo->data); + } + [...] + } + +### CloudinaryHelper +CloudinaryHelper is an extension of the CakePHP InputHelper. It can be used for loading cloudinary\_js, presenting images, creating forms with image inputs and more. + +#### Setup +You can load CloudinaryHelper using two methods - + +`Controller/PhotosController.php`: + + [...] + class PhotosController extends AppController { + // Replace the FormHelper with CloudinaryHelper (recommended - accessible as $this->Form) + public $helpers = array('Html', 'Form' => array('className' => 'CloudinaryCake.Cloudinary')); + + // Add CloudinaryHelper in addition to the default FormHelper (accessible as $this->Cloudinary instead of $this->Form) + //public $helpers = array('Html', 'Form', 'CloudinaryCake.Cloudinary'); + [...] + } + +#### Usage +You then can use it in any view of the controller: + +`View/Layouts/default.ctp`: + + [...] +
+ [...] + # Include cloudinary_js dependencies (requires jQuery) + echo $this->Form->cloudinary_includes(); + # Setup cloudinary_js using the current cloudinary_php configuration + echo cloudinary_js_config(); + [...] + + [...] + +`View/Photos/add.ctp`: + + [...] + + Form->cl_image_tag($photo['Photo']['cloudinaryIdentifier'], + array("width" => 60, "height" => 60, "crop" => "thumb", "gravity" => "face")); ?> + + Form->create('Photo', array('type' => 'file')); ?> + + Form->input('id'); + # Backend upload: + echo $this->Form->input('cloudinaryIdentifier'); + # Direct upload: + #echo $this->Form->input('cloudinaryIdentifier', array("type" => "direct_upload")); + ?> + Form->end(__('Submit')); ?> + [...] + diff --git a/cake_plugin/CloudinaryCake/Test/Case/Controller/Component/empty b/cake_plugin/CloudinaryCake/Test/Case/Controller/Component/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCake/Test/Case/Model/Behavior/empty b/cake_plugin/CloudinaryCake/Test/Case/Model/Behavior/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCake/Test/Case/View/Helper/empty b/cake_plugin/CloudinaryCake/Test/Case/View/Helper/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCake/Test/Fixture/empty b/cake_plugin/CloudinaryCake/Test/Fixture/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCake/Vendor/empty b/cake_plugin/CloudinaryCake/Vendor/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php b/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php new file mode 100644 index 00000000..8955930d --- /dev/null +++ b/cake_plugin/CloudinaryCake/View/Helper/CloudinaryHelper.php @@ -0,0 +1,61 @@ +cloudinaryFunctions)) { + return call_user_func_array($name, $args); + } + return parent::__call($name, $args); + } + + /// Automatically detect cloudinary fields on models that have declared them. + public function input($fieldName, $options = array()) { + $this->setEntity($fieldName); + $model = $this->_getModel($this->model()); + $fieldKey = $this->field(); + if (!@$options['type'] && $model->hasMethod('cloudinaryFields') && in_array($fieldKey, $model->cloudinaryFields())) { + $options['type'] = 'file'; + } + return parent::input($fieldName, $options); + } + + public function cloudinary_includes($options = array()) { + foreach ($this->cloudinaryJSIncludes as $include) { + echo $this->Html->script($include, $options); + } + } + + /// Called for input() when type => direct_upload + public function direct_upload($fieldName, $options = array()) { + $modelKey = $this->model(); + $fieldKey = $this->field(); + $options = @$options["cloudinary"] ? $options["cloudinary"] : array(); + return \cl_image_upload_tag("data[" . $modelKey . "][" . $fieldKey . "]", $options); + } +} diff --git a/cake_plugin/CloudinaryCake/View/Helper/empty b/cake_plugin/CloudinaryCake/View/Helper/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCake/webroot/empty b/cake_plugin/CloudinaryCake/webroot/empty new file mode 100644 index 00000000..e69de29b diff --git a/cake_plugin/CloudinaryCakeLoader.php b/cake_plugin/CloudinaryCakeLoader.php new file mode 100644 index 00000000..d2133ead --- /dev/null +++ b/cake_plugin/CloudinaryCakeLoader.php @@ -0,0 +1,32 @@ + true, 'routes' => false, + 'path' => __DIR__ . DS . 'CloudinaryCake' . DS)); + } + + private static function fixAutoload() { + // Remove and re-prepend CakePHP's autoloader as composer thinks it is the most important. + // See https://github.com/composer/composer/commit/c80cb76b9b5082ecc3e5b53b1050f76bb27b127b + spl_autoload_unregister(array('App', 'load')); + spl_autoload_register(array('App', 'load'), true, true); + } +} diff --git a/composer.json b/composer.json index bd5dd291..c8c9208c 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "issues": "https://github.com/cloudinary/cloudinary_php/issues" }, "autoload": { + "psr-0": { "": "cake_plugin" }, "classmap": ["src"] } } diff --git a/samples/PhotoAlbum/main.php b/samples/PhotoAlbum/main.php index bcf228b7..17736301 100644 --- a/samples/PhotoAlbum/main.php +++ b/samples/PhotoAlbum/main.php @@ -9,14 +9,14 @@ include 'settings.php'; // Global settings - if (array_key_exists('REQUEST_SCHEME', $_SERVER)) { + if (array_key_exists('REQUEST_SCHEME', $_SERVER)) { $cors_location = $_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["SERVER_NAME"] . - dirname($_SERVER["SCRIPT_NAME"]) . "/lib/cloudinary_cors.html"; + dirname($_SERVER["SCRIPT_NAME"]) . "/lib/cloudinary_cors.html"; } else { - $cors_location = "http://" . $_SERVER["HTTP_HOST"] . "/lib/cloudinary_cors.html"; + $cors_location = "http://" . $_SERVER["HTTP_HOST"] . "/lib/cloudinary_cors.html"; } - - $thumbs_params = array("format" => "jpg", "height" => 150, "width" => 150, + + $thumbs_params = array("format" => "jpg", "height" => 150, "width" => 150, "class" => "thumbnail inline"); // Helper functions @@ -30,31 +30,36 @@ function array_to_table($array) { $saved_error_reporting = error_reporting(0); echo "| " . $key . ": | " . $display_value . " |
| " . $key . ": | " . $display_value . " |
+ : + '{$url}'" + ); ?> +
+ 0): + echo $this->element('exception_stack_trace'); +endif; +?> diff --git a/samples/PhotoAlbumCake/View/Errors/error500.ctp b/samples/PhotoAlbumCake/View/Errors/error500.ctp new file mode 100644 index 00000000..9d0161d2 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Errors/error500.ctp @@ -0,0 +1,20 @@ + + ++ : + +
+ 0): + echo $this->element('exception_stack_trace'); +endif; +?> diff --git a/samples/PhotoAlbumCake/View/Helper/AppHelper.php b/samples/PhotoAlbumCake/View/Helper/AppHelper.php new file mode 100644 index 00000000..7b734247 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Helper/AppHelper.php @@ -0,0 +1,26 @@ + + + + +This email was sent using the CakePHP Framework
+ + \ No newline at end of file diff --git a/samples/PhotoAlbumCake/View/Layouts/Emails/text/default.ctp b/samples/PhotoAlbumCake/View/Layouts/Emails/text/default.ctp new file mode 100644 index 00000000..a84762ea --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/Emails/text/default.ctp @@ -0,0 +1,22 @@ + +fetch('content'); ?> + +This email was sent using the CakePHP Framework, http://cakephp.org. diff --git a/samples/PhotoAlbumCake/View/Layouts/ajax.ctp b/samples/PhotoAlbumCake/View/Layouts/ajax.ctp new file mode 100644 index 00000000..8b06e4dc --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/ajax.ctp @@ -0,0 +1,11 @@ + +fetch('content'); ?> diff --git a/samples/PhotoAlbumCake/View/Layouts/default.ctp b/samples/PhotoAlbumCake/View/Layouts/default.ctp new file mode 100644 index 00000000..98a67ab1 --- /dev/null +++ b/samples/PhotoAlbumCake/View/Layouts/default.ctp @@ -0,0 +1,53 @@ + + + + + Html->charset(); ?> ++ + 1) Help me configure it + 2) I don't / can't use URL rewriting +
+ ++=')): + echo ''; + echo __d('cake_dev', 'Your version of PHP is 5.2.8 or higher.'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your version of PHP is too low. You need PHP 5.2.8 or higher to use CakePHP.'); + echo ''; + endif; +?> +
++ '; + echo __d('cake_dev', 'Your tmp directory is writable.'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your tmp directory is NOT writable.'); + echo ''; + endif; + ?> +
++ '; + echo __d('cake_dev', 'The %s is being used for core caching. To change the config edit %s', ''. $settings['engine'] . 'Engine', 'APP/Config/core.php'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your cache is NOT working. Please check the settings in %s', 'APP/Config/core.php'); + echo ''; + endif; + ?> +
+
+ ';
+ echo __d('cake_dev', 'Your database configuration file is present.');
+ $filePresent = true;
+ echo '';
+ else:
+ echo '';
+ echo __d('cake_dev', 'Your database configuration file is NOT present.');
+ echo '
';
+ echo __d('cake_dev', 'Rename %s to %s', 'APP/Config/database.php.default', 'APP/Config/database.php');
+ echo '';
+ endif;
+ ?>
+
+ isConnected()):
+ echo '';
+ echo __d('cake_dev', 'CakePHP is able to connect to the database.');
+ echo '';
+ else:
+ echo '';
+ echo __d('cake_dev', 'CakePHP is NOT able to connect to the database.');
+ echo '
';
+ echo $errorMsg;
+ echo '';
+ endif;
+ ?>
+
--enable-unicode-properties when configuring');
+ echo '';
+ endif;
+?>
+
+
+ ';
+ echo __d('cake_dev', 'DebugKit plugin is present');
+ echo '';
+ else:
+ echo '';
+ echo __d('cake_dev', 'DebugKit is not installed. It will help you inspect and debug different aspects of your application.');
+ echo '
';
+ echo __d('cake_dev', 'You can install it from %s', $this->Html->link('github', 'https://github.com/cakephp/debug_kit'));
+ echo '';
+ endif;
+ ?>
+
+
+To change its layout, edit: %s.
+You can also add some CSS styles for your pages at: %s.',
+ 'APP/View/Pages/home.ctp', 'APP/View/Layouts/default.ctp', 'APP/webroot/css');
+?>
+
+ Html->link( + sprintf('%s %s', __d('cake_dev', 'New'), __d('cake_dev', 'CakePHP 2.0 Docs')), + 'http://book.cakephp.org/2.0/en/', + array('target' => '_blank', 'escape' => false) + ); + ?> +
++ Html->link( + __d('cake_dev', 'The 15 min Blog Tutorial'), + 'http://book.cakephp.org/2.0/en/tutorials-and-examples/blog/blog.html', + array('target' => '_blank', 'escape' => false) + ); + ?> +
+ + ++
+ +
++ +
+ +| Paginator->sort('id'); ?> | +Paginator->sort('cloudinaryIdentifier'); ?> | +Thumbnail | +Paginator->sort('moderated'); ?> | +Paginator->sort('created'); ?> | +Paginator->sort('updated'); ?> | ++ |
|---|---|---|---|---|---|---|
| + | url()) { + echo ''; + $close_tag = ''; + } + echo h($photo['Photo']['cloudinaryIdentifier']); + echo @$close_tag; + ?> | +Cloudinary->cl_image_tag($photo['Photo']['cloudinaryIdentifier'], + array("width" => 60, "height" => 60, "crop" => "thumb", "gravity" => "face")); ?> | ++ | + | + | + Html->link(__('View'), array('action' => 'view', $photo['Photo']['id'])); ?> + Html->link(__('Edit'), array('action' => 'edit', $photo['Photo']['id'])); ?> + Form->postLink(__('Delete'), array('action' => 'delete', $photo['Photo']['id']), null, __('Are you sure you want to delete # %s?', $photo['Photo']['id'])); ?> + | +
+ Paginator->counter(array( + 'format' => __('Page {:page} of {:pages}, showing {:current} records out of {:count} total, starting on record {:start}, ending on {:end}') + )); + ?>
+| " . $key . ": | " . $display_value . " |
+ This is the main demo page of the PhotoAlbum sample PHP application of Cloudinary.
+ Here you can see all images you have uploaded to this PHP application and find some information on how
+ to implement your own PHP application storing, manipulating and serving your photos using Cloudinary!
+
+ All of the images you see here are transformed and served by Cloudinary. + For instance, the logo and the poster frame. + They are both generated in the cloud using the Cloudinary shortcut functions: fetch_image_tag and facebook_profile_image_tag. + These two pictures weren't even have to be uploaded to Cloudinary, they are retrieved by the service, transformed, cached and distributed through a CDN. +
+ ++ Following are the images uploaded by you. You can also upload more pictures. + + You can click on each picture to view its original size, and see more info about and additional transformations. + Html->link('Upload Images...', + array('controller' => 'photos', 'action' => 'upload'), + array('class' => 'upload_link')); + ?> +
+ +No images were uploaded yet.
+ + "; + echo cl_image_tag($photo["Photo"]["cloudinaryIdentifier"], array_merge($thumbs_params, array("crop" => "fill"))); + ?> + + + + +| ";
+ echo " ";
+ echo "" .
+ cl_image_tag($photo["Photo"]["cloudinaryIdentifier"], $merged_params) . "";
+ echo " ";
+ echo ""; + array_to_table($merged_params); + echo " | ";
+ }
+ ?>
+
+