From 37fa7085b02d7299e61c1870a88b129bbfbcebd2 Mon Sep 17 00:00:00 2001 From: "J. Bruni" Date: Tue, 3 Sep 2013 01:08:26 -0300 Subject: [PATCH] Initial commit --- .gitignore | 4 + .travis.yml | 12 + README.md | 203 ++++++++ composer.json | 23 + phpunit.xml | 18 + src/Jbruni/Larnotify/Larnotify.php | 9 + src/Jbruni/Larnotify/NotificationManager.php | 467 ++++++++++++++++++ .../Larnotify/NotificationServiceProvider.php | 73 +++ src/config/config.php | 36 ++ tests/.gitkeep | 0 10 files changed, 845 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 src/Jbruni/Larnotify/Larnotify.php create mode 100644 src/Jbruni/Larnotify/NotificationManager.php create mode 100644 src/Jbruni/Larnotify/NotificationServiceProvider.php create mode 100644 src/config/config.php create mode 100644 tests/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c1fc0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.phar +composer.lock +.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0a1c1cb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + +before_script: + - curl -s http://getcomposer.org/installer | php + - php composer.phar install --dev + +script: phpunit \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0db272b --- /dev/null +++ b/README.md @@ -0,0 +1,203 @@ +Installation +============ + +Use [composer](http://getcomposer.org), at the root of your laravel project: + + composer require jbruni/larnotify + +And then add the service provider to the `providers` array at `app/config/app.php`: + + 'Jbruni\Larnotify\NotificationServiceProvider', + +It is recommended to add an alias to the `aliases` array at the same file: + + 'Larnotify' => 'Jbruni\Larnotify\Larnotify', + + +Usage +===== + +### Basic + +Basically, you add notifications by using the `add` method, from anywhere in your application: + + Larnotify::add('Settings saved.'); + +And anywhere in your views you can generate output: + + {{ $messages }} + +The `add` method accepts an optional first parameter which can specify the message type: + + Larnotify::add('warning', 'You have only 1 credit remaining.'); + Larnotify::add('error', 'Failed to get the selected item.'); + +And to render the specific message type: + + {{ $messages['warning'] }} + +You can also namespace your messages. For example, in Facebook you have "friend requests", "messages", and "notifications" - three distinct block of notifications... + + Larnotify::add('requests.unread', 'John wants to be your friend.'); + Larnotify::add('requests.read', 'Mary wants to be your friend.'); + Larnotify::add('messages.new', 'Hi! How are you?'); + Larnotify::add('notifications.liked', 'Pete liked your post.'); + +And where it is time to output... + + {{ $messages['requests.ALL'] }} + {{ $messages['messages.new'] }} + +In fact, EVERY message has a **namespace** and a **message type**, even when not specified. + +The default namespace is "default" and the default message type is "msg". + +So, in the first two examples (above), the messages were stored at "default.msg" and "default.warning" namespaces/types. + +### Formatting output + +There are two special message types: "sprintf" and "view": + + Larnotify::add('sprintf:Your name is %s.', 'John Doe'); + Larnotify::add('view:paused_service', array('date' => '10/10/2013')); + +The first example is rendered using "sprintf" command. The second argument needs to be an array of the remaining "sprintf" parameters, or a single string. + +The "view" type allows you to specify a **view** name, and its parameters. + +Both message types accept a namespace. + + Larnotify::add('info.sprintf:Hello, %s! You have earned %s points.', array('Mary Jane', '100')); + +The above notification will be stored at "info.sprintf" namespace/type and will be rendered using: + + sprintf('Hello, %s! You have earned %s points.', 'Mary Jane', '100'); + +Now, an example using `view`: + + Larnotify::add('info.view:user.balance', array('amount' => '108.90'); + +This will available at "info.view" and will be rendered using: + + View::make('user.balance', array('amount' => '108.90')); + +Both shall be rendered at once, if called at the same request, since they belong to the same namespace: + + {{ $messages['info'] }} + +If you want to select one of the types: + + {{ $messages['info.sprintf'] }} + {{ $messages['info.view'] }} + +#### Group templates + +As we've seen, these "view" and "sprintf" templates are specified for single notifications. + +It is possible to specify a single template which will render all its assigned notifications. + +Example: + + Larnotify::add('user.info', 'Account successfully created.'); + Larnotify::add('user.info', 'You have earned 200 bonus points.'); + +Through configuration, either at the config file or at runtime, you can assign a **view** to render them: + + // config + 'views' => array( + 'user.info' => 'infowidget' + ); + + //runtime + Larnotify::setView('user.info', 'infowidget'); + +(NOTE: If a "user.info" view exists, it will be automatically used. No configuration or "setView" needed.) + +An array with the corresponding notifications will be available at the `$notifications` variable for the 'infowidget' view. + +You can loop through them and render as you want. + +The result will be available at + + {{ $messages['user.info] }} + +Note that nothing prevents you from sending arrays instead of strings as messages. This allows you to further process the notifications in your view: + + Larnotify::add('commits.latest', array('repository_name' => 'Larnotify', 'hashes' => array('af12ca72', 'b7m2o018', 'abcdef78'))); + Larnotify::add(array('author' => 'Taylor', 'tweet' => 'Well done!')); + +#### JSON + +Instead of rendering HTML and including it in a template, or sending it as an AJAX response to be inserted into the DOM, you may be already dealing with a robust front-end application, using Angular.JS or similar, and you just want raw data, because you will be doing all the DOM magic through client-side Javascript... + +In this or any other case, you can have the messages, or the template data, with no rendering, in JSON format: + + Larnotify::getJson('requests.all'); + +Or in a template: + + {{ $messages->getJson('user.info'); }} + +You now start to think and feel that Larnotify can be used far beyond notifications... don't you? + +Configuration +============= + +Here is the current config file: + + array( + 'default.msg' => '', + ), + + /** + * string Global view variable name (Larnotify object) + */ + 'view_share' => 'messages', + + /** + * string Notifications variable name (Array of messages) + */ + 'msg_variable' => 'notifications', + + /** + * string Use this sprintf template to render messages if none is provided + */ + 'default_template' => '

%1$s

', + + /** + * string String to be included between each block of rendered output + * + * NOTE: "\n" is recommended + */ + 'block_splitter' => '', + + ); + +If no template is available, the `default_template` will be used as sprintf argument, receiving the message, the namespace, and the message type. + +So, out of the box, `Larnotify::add('You won!');` renders as: + +

You won!

+ +You may want to tackle directly with CSS, instead of anything else. + +----- + +The global variable available in all views to access Larnotify is `$messages` but you can change it through `view_share` configuration option. + +Similarly, the messages will be available for rendering group templates as `$notifications`, but you can change it through `msg_variable` configuration option. + +Finally, the `block_splitter` string is included when rendering, between each block rendered by Larnotify. I will always use "\n", so when looking at generated HTML source, each notification starts in a new line. But this may not be the desired behaviour, so this "magic" is turned off by default. + +We have already covered the `views` configuration option in the "Group Templates" section. It is an array, where the key is the message type with or without a prefixed namespace, and the value is a view template name. + +#### Thank you. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..aca08f0 --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "jbruni/larnotify", + "description": "Notification Services for Laravel 4", + "keywords": ["notification", "messages", "laravel"], + "homepage": "http://github.com/jbruni/larnotify", + "license": "MIT", + "authors": [ + { + "name": "J. Bruni", + "email": "contato@jbruni.com.br" + } + ], + "require": { + "php": ">=5.3.0", + "illuminate/support": "4.0.x" + }, + "autoload": { + "psr-0": { + "Jbruni\\Larnotify": "src/" + } + }, + "minimum-stability": "dev" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e89ac6d --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + \ No newline at end of file diff --git a/src/Jbruni/Larnotify/Larnotify.php b/src/Jbruni/Larnotify/Larnotify.php new file mode 100644 index 0000000..31a9aa4 --- /dev/null +++ b/src/Jbruni/Larnotify/Larnotify.php @@ -0,0 +1,9 @@ +app = $app; + } + + /** + * @return NotificationManager Allow fluent syntax using Facade + */ + public function get() + { + return $this; + } + + /** + * @return array Currently existent messageBag names + */ + public function getBags() + { + return array_unique(array_merge(array('default'), array_keys($this->messages))); + } + + /** + * @param boolean $prefixBag True returns "default.msg", "bag.type", etc; False (default) returns "msg", "type", etc + * @return array Currently existent message types + */ + public function getTypes($prefixBag = FALSE) + { + $types = array(); + foreach ($this->messages as $bag => $messageBag) + { + foreach (array_keys($messageBag->getMessages()) as $bagType) + { + $types[] = ($prefixBag ? "$bag." : '') . $bagType; + } + } + return array_unique(array_merge(array(($prefixBag ? 'default.' : '') . 'msg'), $types)); + } + + /** + * @param string $bag Bag name + * @return array Currently existent message types in the speficied bag + */ + public function getBagTypes($bag) + { + $bagTypes = array_keys($this->getMessageBag($bag, self::CREATE_DETTACHED)->getMessages()); + + if ($bag == 'default') + { + $bagTypes = array_unique(array_merge(array('msg'), $bagTypes)); + } + + return $bagTypes; + } + + /** + * This Notification Manager allows a kind of "namespacing" the messages and also selecting a "type" for each message + * It always use "default" namespace when none is provided, and "msg" type when none is provided + * To provide a different type, just use it directly: "error", "warning", "info" [the "default" namespace will be used] + * To provide a different namespace, append it to the message type: "requests.unread", "requests.read" + * To provide only the namespace, leave the type empty: "request." [the default "msg" type will be used] + * + * @param string $where The namespace and type specification (read above) + * @return array Array with two strings: first is the "bagName" (namespace) and second is "messageType" (type) + */ + public function getBagType($where = 'default.msg') + { + if (strpos($where, '.') === FALSE) + { + $where = 'default.' . $where; + } + + $result = explode('.', $where, 2); + + if (empty($result[1])) + { + $result[1] = 'msg'; + } + + return $result; + } + + /** + * Add a notification + * + * The first parameter is optional. If provided, must be a string. It can specify: + * 1) A "sprintf" template. Example: "sprintf:Your name is %s and you are %s" + * 2) A "view" name. Example: "view:messages.error" + * 3) A message type, with optional namespace. Examples: "warning", "requests.unread" + * First parameter defaults to "default.msg" ("default" namespace w/ default "msg" type) + * + * Both "sprintf" and "view" are "reserved" message types. + * You can attach a namespace to the "sprintf" or "view" message types. Just prefix it. Example: + * Larnotify::add('widget.view:new_call', array('from' => 'Best Friend', 'when' => '10 minutes ago')); + * + * The other parameter contains the message specific contents. + * It depends on what has been optionally set by the first parameter: + * 1) A STRING or an ARRAY containing the remaining "sprintf" arguments. Example 1: "J. Bruni" Example 2: array("J. Bruni", "male") + * 2) An ARRAY containing the template data. Example: array("name" => "J. Bruni", "country" => "Brazil") + * 3) You can specify a STRING containing a single message or an ARRAY of string messages. + * + * @param string $template OPTIONAL (see above) + * @param string|array $message Message contents (see above) + */ + public function add($template = '', $message = '') + { + $multiple = FALSE; + $where = $template; + + if (func_num_args() < 2) + { + $message = $template; + $where = 'default.msg'; + $multiple = is_array($message); + } + + if (preg_match('/^(([^.]+\.)?view):(.*)$/', $template, $matches) === 1) + { + $where = $matches[1]; + $message = array( + 'view' => $matches[3], + 'data' => $message, + ); + } + + if (preg_match('/^(([^.]+\.)?sprintf):(.*)$/', $template, $matches) === 1) + { + $where = $matches[1]; + $message = array_merge( + array($matches[3]), + (array) $message + ); + } + + list($bag, $type) = $this->getBagType($where); + + $messageBag = $this->getMessageBag($bag, self::CREATE_ATTACHED); + + if (!$multiple) + { + return $messageBag->add($type, $message); + } + + foreach ($message as $msg) + { + $result = $messageBag->add($type, $msg); + } + + return $result; + } + + /** + * Retrieve notifications + * + * @param string $where Optional "namespace.type" (see "getBagType" documentation above) + * @return array Notification messages + */ + public function getMessages($where = 'default.ALL') + { + list($bag, $type) = $this->getBagType($where); + + $messagesArray = $this->getMessageBag($bag, self::CREATE_DETTACHED)->getMessages(); + + if ($type == 'ALL') { return $messagesArray; } + + return (isset($messagesArray[$type]) ? $messagesArray[$type] : array()); + } + + /** + * Retrieve notifications in JSON format + * + * @param string $where Optional "namespace.type" (see "getBagType" documentation above) + * @param integer $options Options for the "json_encode" function + * @return string Notification messages (JSON encoded) + */ + public function getJson($where = 'default.ALL', $options = 0) + { + return json_encode($this->getMessages($where), $options); + } + + /** + * @param string $where Optional "namespace.type" (see "getBagType" documentation above) + * @return string Rendered view for the messages + */ + public function render($where = 'default.ALL') + { + $messages = $this->getMessages($where); + + list($bag, $type) = $this->getBagType($where); + + if ($type != 'ALL') + { + return $this->renderMessages($messages, $bag, $type); + } + + $contents = array(); + foreach (array_keys($messages) as $type) + { + $contents[] = $this->renderMessages($messages[$type], $bag, $type); + } + return implode($this->getBlockSplitter(), $contents); + } + + /** + * @param array $messages Messages retrieved from bag + * @param string $bag Bag name (namespace) + * @param string $type Messages type + * @return string Rendered messages + */ + public function renderMessages($messages, $bag, $type) + { + if (empty($messages)) { return ''; } + + $contents = array(); + + switch($type) + { + case 'sprintf': + foreach ($messages as $message) + { + $contents[] = call_user_func_array('sprintf', $message); + } + break; + + case 'view': + foreach ($messages as $message) + { + $contents[] = $this->app->make('view')->make($message['view'], $message['data']); + } + break; + + default: + $view = $this->getViewFor($bag, $type); + + if (!empty($view) && $this->app->make('view')->exists($view)) + { + $template_variable = $this->app->make('config')->get('larnotify::msg_variable'); + return $this->app->make('view')->make($view, array($template_variable => $messages)); + } + + foreach ($messages as $message) + { + $contents[] = sprintf($this->getDefaultTemplate(), $message, $bag, $type); + } + } + + return implode($this->getBlockSplitter(), $contents); + } + + /** + * @param string $bag Bag name (namespace) + * @param string $type Message type + * @return string Configured view + */ + public function getViewFor($bag = 'default', $type = 'msg') + { + if ($this->app->make('view')->exists("$bag.$type")) return "$bag.$type"; + + $views = $this->app->make('config')->get('larnotify::views'); + + if (isset($views["$bag.$type"])) { return $views["$bag.$type"]; } + + if (isset($views["$type"])) { return $views["$type"]; } + + if (isset($views["default.$type"])) { return $views["default.$type"]; } + + if (isset($views['msg'])) { return $views['msg']; } + + if (isset($views['default.msg'])) { return $views['default.msg']; } + + return ''; + } + + /** + * @return string Configured default template (argument for sprintf) + */ + public function getDefaultTemplate() + { + return $this->app->make('config')->get('larnotify::default_template', '%s'); + } + + /** + * @return string Configured block splitter + */ + public function getBlockSplitter() + { + return $this->app->make('config')->get('larnotify::block_splitter', ''); + } + + /** + * @param string $where Notification "namespace.type" (see "getBagType" documentation above) + * @param string $view View template name + */ + public function setTemplate($where, $view) + { + $views = $this->app->make('config')->get('larnotify::views'); + $views[$where] = $view; + $this->app->make('config')->set('larnotify::views', $views); + } + + /** + * @param string $where Notification "namespace.type" (see "getBagType" documentation above) + */ + public function unsetTemplate($where) + { + $views = $this->app->make('config')->get('larnotify::views'); + unset($views[$where]); + $this->app->make('config')->set('larnotify::views', $views); + } + + /** + * This helper allows easy inclusion of $messages in templates + * + * @return string Generated View + */ + public function __toString() + { + return $this->render(); + } + + /* + |-------------------------------------------------------------------------- + | ArrayableInterface implementation + |-------------------------------------------------------------------------- + */ + + public function toArray() + { + return $this->getMessages(); + } + + /* + |-------------------------------------------------------------------------- + | JsonableInterface implementation + |-------------------------------------------------------------------------- + */ + + public function toJson($options = 0) + { + return $this->getJson('default.ALL', $options); + } + + /* + |-------------------------------------------------------------------------- + | MessageProviderInterface implementation + |-------------------------------------------------------------------------- + */ + + public function getMessageBag($bag = 'default', $mode = self::DO_NOT_CREATE) + { + if (isset($this->messages[$bag])) + { + return $this->messages[$bag]; + } + + if ($bag == 'default') + { + $mode = self::CREATE_ATTACHED; + } + + if ($mode === self::DO_NOT_CREATE) + { + return; + } + + $messageBag = new MessageBag(); + + if ($mode === self::CREATE_ATTACHED) + { + $this->messages[$bag] = $messageBag; + } + + return $messageBag; + } + + /* + |-------------------------------------------------------------------------- + | Countable interface implementation + |-------------------------------------------------------------------------- + */ + + public function count() + { + $count = 0; + foreach ($this->messages as $messageBag) + { + $count += count($messageBag->all()); + } + return $count; + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess interface implementation + |-------------------------------------------------------------------------- + */ + + public function offsetSet($where, $message) + { + $this->add($message, $where); + } + + public function offsetExists($where) + { + list($bag, $type) = $this->getBagType($where); + + if (($bag == 'default') && ($type == 'msg')) { return TRUE; } + + return isset($this->messages[$bag][$type]); + } + + public function offsetUnset($where) + { + list($bag, $type) = $this->getBagType($where); + + if (isset($this->messages[$bag][$type])) + { + unset($this->messages[$bag][$type]); + } + } + + public function offsetGet($where) + { + return $this->render($where); + } +} diff --git a/src/Jbruni/Larnotify/NotificationServiceProvider.php b/src/Jbruni/Larnotify/NotificationServiceProvider.php new file mode 100644 index 0000000..b04eb9d --- /dev/null +++ b/src/Jbruni/Larnotify/NotificationServiceProvider.php @@ -0,0 +1,73 @@ +package('jbruni/larnotify'); + $this->registerNotifyEvents(); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + $manager = $this->manager = new NotificationManager($this->app); + + $this->app['larnotify'] = $this->app->share(function() use ($manager) + { + return $manager; + }); + } + + /** + * Register the events needed for notification. + * + * @return void + */ + protected function registerNotifyEvents() + { + $app = $this->app; + $manager = $this->manager; + + $app->before(function($request) use ($app, $manager) + { + $app->make('view')->share($app->make('config')->get('larnotify::view_share'), $manager); + }); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return array('larnotify'); + } + +} diff --git a/src/config/config.php b/src/config/config.php new file mode 100644 index 0000000..6debaeb --- /dev/null +++ b/src/config/config.php @@ -0,0 +1,36 @@ + array( + 'default.msg' => '', + ), + + /** + * string Global view variable name (Larnotify object) + */ + 'view_share' => 'messages', + + /** + * string Notifications variable name (Array of messages) + */ + 'msg_variable' => 'notifications', + + /** + * string Use this sprintf template to render messages if none is provided + */ + 'default_template' => '

%1$s

', + + /** + * string String to be included between each block of rendered output + * + * NOTE: "\n" is recommended + */ + 'block_splitter' => '', + +); diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29