├── tests
└── .gitkeep
├── src
├── config
│ ├── .gitkeep
│ └── firebase.php
└── j42
│ └── LaravelFirebase
│ ├── LaravelFirebaseFacade.php
│ ├── Token
│ ├── TokenFacade.php
│ ├── TokenInterface.php
│ └── Token.php
│ ├── LaravelFirebaseServiceProvider.php
│ └── Client.php
├── .gitignore
├── .travis.yml
├── phpunit.xml
├── composer.json
├── LICENSE
└── README.md
/tests/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/config/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | composer.phar
3 | composer.lock
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/src/config/firebase.php:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | ./tests/
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "j42/laravel-firebase",
3 | "description": "A Firebase port for Laravel (4.2+)",
4 | "authors": [
5 | {
6 | "name": "Julian Aceves",
7 | "email": "j@j42.me",
8 | "homepage": "https://j42.me"
9 | }
10 | ],
11 | "repositories": [
12 | {
13 | "type": "vcs",
14 | "url": "https://github.com/firebase/php-jwt"
15 | }
16 | ],
17 | "require": {
18 | "php": ">=5.4.0",
19 | "guzzlehttp/guzzle": "6.1.*",
20 | "firebase/php-jwt": "@dev"
21 | },
22 | "autoload": {
23 | "classmap": [
24 | "src/j42"
25 | ],
26 | "psr-0": {
27 | "J42\\LaravelFirebase": "src/j42/LaravelFirebase"
28 | }
29 | },
30 | "config": {
31 | "preferred-install": "dist"
32 | },
33 | "minimum-stability": "stable",
34 | "prefer-stable": true
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Julian Aceves
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/j42/LaravelFirebase/Token/Token.php:
--------------------------------------------------------------------------------
1 | secret = $secret;
10 | }
11 |
12 |
13 | // Return: (string) JSON Web Token
14 | // Args: (Array) $data, (Array) $options
15 | /* $options
16 | [admin] Bypass all security rules? (Default: false)
17 | *
18 | [debug] Enable debug output from security rules? (Default: false)
19 | *
20 | [expires] (int || DateTime) When token should expire
21 | *
22 | [notBefore] (int || DateTime) Only valid after this time
23 | */
24 | public function create(Array $data, Array $options = null) {
25 |
26 | $json = json_encode($data);
27 | $jsonError = (function_exists("json_last_error") && $errno = json_last_error());
28 | $jsonInvalid = ($json === 'null');
29 |
30 | // Handle Errors
31 | if ($jsonError) static::error($errno); elseif ($jsonInvalid) static::error(JSON_ERROR_SYNTAX);
32 |
33 | // Build Claims
34 | $claims = (is_array($options)) ? $this->configure($options) : [];
35 | $claims += [
36 | 'd' => $data,
37 | 'v' => 0.1,
38 | 'iat' => time()
39 | ];
40 |
41 | // Return JWT
42 | return \JWT::encode($claims, $this->secret, 'HS256');
43 |
44 | }
45 |
46 |
47 | // [STA]
48 | // Return: (Array) JSON Web Token Meta-Options
49 | // Args: (Array) $config
50 | private static function configure(Array $options) {
51 | $claims = [];
52 | foreach ($options as $key => $value) {
53 |
54 | // Parse Options
55 | switch ($key) {
56 | case 'admin': $claims['admin'] = $value; break;
57 | case 'debug': $claims['debug'] = $value; break;
58 | case 'expires':
59 | case 'notBefore':
60 | $code = ($key === 'notBefore') ? 'nbf' : 'exp';
61 |
62 | // (DateTime || int) ?
63 | switch(gettype($value)) {
64 | case 'integer':
65 | $claims[$code] = $value;
66 | break;
67 | case 'object':
68 | if ($value instanceOf \DateTime) $claims[$code] = $value->getTimestamp(); else $this->error(403);
69 | break;
70 | default:
71 | static::error(403);
72 | }
73 | break;
74 |
75 | default: static::error(403);
76 | }
77 |
78 | }
79 | return $claims;
80 | }
81 |
82 |
83 | // [STA]
84 | // Return: void
85 | // Args: (int) $errno
86 | private static function error($n) {
87 |
88 | $messages = [
89 | JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
90 | JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
91 | JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
92 | static::FIREBASE_GENERAL => 'Firebase encountered an unknown error',
93 | static::INVALID_OPTION => 'The Token Configuration Service encountered an invalid option in the configuration'
94 | ];
95 |
96 | throw new \UnexpectedValueException($messages[$n] ?: 'Unknown Error: '.$n);
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/j42/LaravelFirebase/LaravelFirebaseServiceProvider.php:
--------------------------------------------------------------------------------
1 | sync($obj); }, 10);
29 | Event::listen('eloquent.updated: *', function($obj) use ($self) { return $self->sync($obj); }, 10);
30 | Event::listen('eloquent.deleted: *', function($obj) use ($self) { return $self->delete($obj); }, 10);
31 | }
32 |
33 |
34 |
35 | /**
36 | * Register the service provider.
37 | * @return void
38 | */
39 | public function register() {
40 |
41 | // Register Package Configuration
42 | $this->publishes([
43 | __DIR__.'/../../../config/firebase.php' => config_path('firebase.php')
44 | ], 'config');
45 |
46 | // Get Connection
47 | $config = (!empty(config('firebase')))
48 | ? config('firebase')
49 | : config('database.connections.firebase');
50 |
51 | // Root provider
52 | $this->app->singleton('firebase', function($app) use ($config) {
53 | return new Client($config);
54 | });
55 |
56 | // Token Provider
57 | $this->app->bind('firebase.token', function($app) use ($config) {
58 | return new Token($config['token']);
59 | });
60 |
61 | }
62 |
63 |
64 | /**
65 | * Get the services provided by the provider.
66 | *
67 | * @return array
68 | */
69 | public function provides() {
70 | return ['firebase', 'firebase.token'];
71 | }
72 |
73 |
74 | /**
75 | * Sync handler
76 | * @param object $obj
77 | * @return void
78 | */
79 | private function sync($obj) {
80 |
81 | $sync = (!empty(config('firebase')))
82 | ? config('firebase.sync')
83 | : config('database.connections.firebase.sync'); // `sync` by Default (config)?
84 | $path = strtolower(get_class($obj)).'s'; // plural collection name
85 | $id = \Firebase::getId($obj); // object ID (extracted)
86 |
87 | // Whitelist
88 | if (isset($obj->firebase) && !empty($obj->firebase) && is_array($obj->firebase)) {
89 | $data = [];
90 | foreach ($obj->toArray() as $key => $value) {
91 | // Filter Attributes
92 | if (in_array($key, $obj->firebase) !== false) $data[$key] = $value;
93 | }
94 | } else {
95 | $data = $obj->toArray();
96 | }
97 |
98 | // Post if Allowed
99 | if ((($sync !== false || !empty($obj->firebase)) && $obj->firebase !== false) || $obj->firebase === true) {
100 | \Firebase::set('/'.$path.'/'.$id, $data);
101 | }
102 |
103 | return true;
104 | }
105 |
106 |
107 | /**
108 | * Delete handler
109 | * @param object $obj
110 | * @return void
111 | */
112 | private function delete($obj) {
113 |
114 | $sync = (!empty(config('firebase')))
115 | ? config('firebase.sync')
116 | : config('database.connections.firebase.sync'); // `sync` by Default (config)?
117 | $path = strtolower(get_class($obj)).'s'; // plural collection name
118 | $id = \Firebase::getId($obj); // object ID (extracted)
119 |
120 | // Delete if Allowed
121 | if ($sync !== false || !empty($obj->firebase)) {
122 | \Firebase::delete('/'.$path.'/'.$id);
123 | }
124 | }
125 |
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | laravel-firebase
2 | ================
3 |
4 | A Firebase port for Laravel (4.2+)
5 |
6 |
7 | ##Configuration
8 |
9 | Install via composer. If you have `minimum-stability` set to `stable`, you should add a `@beta` or `@dev` in order to use the `php-jwt` library (a dependency managed by firebase for generating JSON web token).
10 |
11 | Add the following line to your `composer.json` and run composer update:
12 |
13 | {
14 | "require": {
15 | "j42/laravel-firebase": "dev-master"
16 | }
17 | }
18 |
19 | Then add the service providers and facades to `config/app.php`
20 |
21 | 'J42\LaravelFirebase\LaravelFirebaseServiceProvider',
22 |
23 | ...
24 |
25 | 'Firebase' => 'J42\LaravelFirebase\LaravelFirebaseFacade'
26 |
27 |
28 | Access Tokens
29 | ----
30 |
31 | Finally, you should configure your firebase connection in the `config/database.php` array. There are two ways you can define this:
32 |
33 | ####Simple Access Token
34 |
35 | ```php
36 | 'firebase' => array(
37 | 'host' => 'https://.firebaseio.com/',
38 | 'token' => '',
39 | 'timeout' => 10,
40 | 'sync' => false, // OPTIONAL: auto-sync all Eloquent models with Firebase?
41 | )
42 | ```
43 |
44 | ####Advanced: Request a JWT
45 |
46 | This accepts any of the standard options allowed by the firebase [security rules](https://www.firebase.com/docs/security/security-rules.html) and will generate a JSON Web Token for more granular authentication (subject to auth security rules and expirations).
47 |
48 | ```php
49 | 'firebase' => array(
50 | 'host' => 'https://servicerunner.firebaseio.com/',
51 | 'token' => [
52 | 'secret' => '',
53 | 'options' => [
54 | 'auth' => [
55 | 'email' => 'example@yoursite.com'
56 | ]
57 | ],
58 | 'data' => []
59 | ],
60 | 'timeout' => 10,
61 | 'sync' => false, // OPTIONAL: auto-sync all Eloquent models with Firebase?
62 | )
63 | ```
64 |
65 |
66 | The **FirebaseClient** instance is loaded into the IoC container as a singleton, containing a Guzzle instance used to interact with Firebase.
67 |
68 |
69 | Getting Started
70 | ----
71 |
72 | Making simple get requests:
73 |
74 | ```php
75 | // Returns: (Array) of data items
76 | Firebase::get('/my/path');
77 |
78 | // Returns: (\Illuminate\Database\Eloquent\Collection) Eloquent collection of Eloquent models
79 | Firebase::get('/my/path', 'ValidEloquentModelClass');
80 |
81 | // Returns: (\Illuminate\Database\Eloquent\Model) Single Eloquent model
82 | // Conditions: $SomeModelInstance must inherit from Eloquent at some point, and have a (id, _id, or $id) property
83 | Firebase::get($SomeModelInstance);
84 |
85 |
86 | // Returns: (Array) Firebase response
87 | Firebase::set('/my/path', $data);
88 |
89 | // Returns: (Array) Firebase response
90 | Firebase::push('/my/path', $data);
91 |
92 | // Returns: (Array) Firebase response
93 | Firebase::delete('/my/path');
94 | ```
95 |
96 |
97 | Model Syncing
98 | ----
99 |
100 | By default this package will keep your Eloquent models in sync with Firebase. That means that whenever `eloquent.updated: *` is fired, the model will be pushed to Firebase.
101 |
102 | This package will automatically look for 'id', '_id', and '$id' variables on the model so that Firebase paths are normalized like so:
103 |
104 | ```php
105 |
106 | // Eloquent model: User
107 | // Firebase location: /users/{user::id}
108 |
109 | $User = new User(['name' => 'Julian']);
110 |
111 | $User->save(); // Pushed to firebase
112 |
113 | $Copy = Firebase::get('/users/'.$User->id, 'User'); // === copy of $User
114 | $Copy = Firebase::get($User); // === copy of $User
115 |
116 | ```
117 |
118 | To disable this, please ensure `'sync' => false` in your database.connections.firebase configuration array.
119 |
120 | This works with any package that overwrites the default Eloquent model SO LONG AS it is configured to fire the appropriate `saved` and `updated` events. At the moment it is tested with the base `Illuminate...Model` as well as the [Jenssegers MongoDB Eloquent Model](https://github.com/jenssegers/laravel-mongodb)
121 |
122 | ####Syncing Models Individually
123 |
124 | If you want to add a whitelist of properties to push to firebase automatically whenever a model is **updated**, you can do so by adding a whitelist of properties to any supported model.
125 |
126 | This action happens regardless of the (automatic) `sync` property in your configuration array. If the `$firebase` whitelist array is found, then the fields contained will be posted on every update event.
127 |
128 | ```php
129 |
130 | class User extends Eloquent {
131 | ...
132 | public $firebase = ['public_property','name','created']; // These properties are pushed to firebase every time the model is updated
133 | }
134 |
135 | ```
136 |
137 |
138 | ##Advanced Use
139 |
140 | #####Create a token manually
141 |
142 | ```php
143 | $FirebaseTokenGenerator = new J42\LaravelFirebase\FirebaseToken(FIREBASE_SECRET);
144 | $Firebase = App::make('firebase');
145 |
146 | $token = $FirebaseTokenGenerator->create($data, $options);
147 |
148 | $Firebase->setToken($token);
149 | ```
--------------------------------------------------------------------------------
/src/j42/LaravelFirebase/Client.php:
--------------------------------------------------------------------------------
1 | -1) throw new \UnexpectedValueException('Please use HTTPS for all Firebase URLs.');
25 |
26 | // Set Host URI
27 | $this->setHost($config['host']);
28 |
29 | // Set Secret
30 | $this->setToken($config['token'] ?: null);
31 |
32 | // Set Timeout
33 | $this->setTimeout($config['timeout'] ?: 10);
34 |
35 | // Http client
36 | $this->http = new Client();
37 |
38 | }
39 |
40 |
41 | // Return: (json) Firebase Response
42 | // Args: (string) $path, (mixed) $data
43 | public function __call($func, $args) {
44 |
45 | // Errors
46 | if (!in_array($func, $this->passthrough)) throw new \UnexpectedValueException('Unexpected method called');
47 | if (count($args) < 1) throw new \UnexpectedValueException('Not enough arguments');
48 |
49 | // Process URL/Path
50 | $url = $this->absolutePath($args[0]);
51 |
52 | // Write Methods
53 | switch ($func) {
54 |
55 | case 'get':
56 | // Read Data
57 | $requestType = 'GET';
58 | return $this->read($url, (isset($args[1]) ? $args[1] : false));
59 | break;
60 |
61 | case 'set': $requestType = 'PUT'; break;
62 | case 'push': $requestType = 'POST'; break;
63 | case 'update': $requestType = 'PATCH'; break;
64 | }
65 |
66 | // Else Write Data
67 | return ($requestType) ? $this->write($url, $args[1], $requestType) : null;
68 |
69 | }
70 |
71 |
72 |
73 |
74 | // Return: (json) Firebase Response
75 | // Args: void
76 | public function setWithPriority($path, Array $data, $priority) {
77 | $url = $this->absolutePath($path);
78 | $data['.priority'] = $priority;
79 | // Return Response
80 | return $this->write($url, $data, 'PUT');
81 | }
82 |
83 | // Return: (Guzzle) Firebase Response
84 | // Args: (string) $path
85 | public function delete($path) {
86 | // Process Request
87 | $url = $this->absolutePath($path);
88 | $request = $this->http->request('DELETE', $url, []);
89 | return $this->validateResponse($request)->getBody();
90 | }
91 |
92 |
93 | // Return: (Array) Firebase Response || (Illuminate\Database\Eloquent\Collection) Eloquent Collection
94 | // Args: (string) $path
95 | public function read($path, $eloquentCollection = false) {
96 |
97 | // Process Request
98 | $request = $this->http->request('GET', $path, []);
99 | $response = $this->validateResponse($request)->getBody();
100 |
101 | // Is Response Valid?
102 | return ($eloquentCollection) ? $this->makeCollection($response, $eloquentCollection) : $response;
103 | }
104 |
105 |
106 | // Return: (Illuminate\Database\Eloquent\Collection) Eloquent Collection
107 | // Args: (Array) $response, (string) $eloquentModel
108 | public function makeCollection(Array $response, $eloquentModel) {
109 |
110 | // Sanity Check
111 | if (!class_exists($eloquentModel)) return Collection::make($response);
112 |
113 | // Get IDs
114 | $ids = [];
115 | foreach ($response as $id => $object) {
116 | $ids[] = $id;
117 | $ids[] = self::getId($object);
118 | }
119 |
120 | // Return Collection
121 | return call_user_func_array($eloquentModel.'::whereIn', ['_id', $ids]);
122 | }
123 |
124 |
125 | // Return: (Guzzle) Firebase Response
126 | // Args: (string) $path, (Array || Object) $data, (string) $method
127 | public function write($path, $data, $method = 'PUT') {
128 |
129 | // Sanity Check
130 | if (is_object($data)) $data = $data->toArray();
131 |
132 | // JSON.stringify $data
133 | $json = json_encode($data);
134 |
135 | // Sanity Check
136 | if ($json === 'null') throw new \UnexpectedValueException('Data Error: Invalid json/write request');
137 |
138 | // Format Request
139 | $cleaned = self::clean($data);
140 | if (is_array($cleaned) && !isset($cleaned['.priority'])) {
141 | $cleaned['.priority'] = time();
142 | }
143 |
144 | // Process Request
145 | $request = $this->http->request($method, $path, ['json' => $cleaned]);
146 |
147 | // Is Response Valid?
148 | return $this->validateResponse($request)->getBody();
149 | }
150 |
151 |
152 | // Return: void
153 | // Args: (string) $host
154 | public function setHost($host) {
155 | $host .= (substr($host,-1) === '/') ? '' : '/';
156 | $this->host = $host;
157 | }
158 |
159 |
160 | // Return: void
161 | // Args: (string) $timeout
162 | public function setTimeout($timeout) {
163 | if (is_numeric($timeout)) $this->timeout = $timeout;
164 | }
165 |
166 |
167 | // Return: void
168 | // Args: (string || array || bool) $token
169 | public function setToken($token) {
170 |
171 | // Token is Array('secret','options')
172 | if (is_array($token) && isset($token['secret']) && isset($token['options'])) {
173 | // Generate Firebase JSON Web Token
174 | $FirebaseToken = new FirebaseToken($token['secret']);
175 | $FirebaseData = $token['data'] ?: [];
176 | $LocalData = [
177 | 'user' => App::environment()
178 | ];
179 |
180 | // Set Token
181 | $this->token = $FirebaseToken->create($LocalData + $FirebaseData, $token['options']);
182 |
183 | } elseif (is_string($token)) {
184 | // Token is a string secret
185 | $this->token = $token;
186 | } else {
187 | throw new \UnexpectedValueException('Token was not a valid configuration array (secret, options[, data]) or string');
188 | }
189 | }
190 |
191 |
192 | // Return: (string) Absolute URL Path
193 | // Args: (mixed) $path
194 | private function absolutePath($item) {
195 |
196 | // Sanity Check
197 | if (!is_string($item) && !$item instanceOf Model) throw new \UnexpectedValueException('Path should be a string or object.');
198 |
199 | // Item is already a fully-qualified URL
200 | if ((strpos($item, 'https://') !== false)) return $item;
201 |
202 | // Else, build URL
203 | $url = $this->host;
204 |
205 | // Path from Item
206 | if (is_string($item)) $path = ltrim($item, '/');
207 | if ($item instanceOf Model) $path = strtolower(get_class($item)).'s/'.self::getId($item);
208 |
209 | // Return URL
210 | $auth = (!empty($this->token)) ? '?'.http_build_query(['auth' => $this->token]) : '';
211 | return $url.$path.'.json'.$auth;
212 | }
213 |
214 |
215 | // Return: (Guzzle) Response
216 | // Args: (Guzzle) Response
217 | private function validateResponse($response) {
218 | if ($response->getStatusCode() == 200) {
219 | return $response;
220 | } else throw new \Exception('HTTP Error: '.$response->getReasonPhrase());
221 | }
222 |
223 |
224 |
225 | // [STA]
226 | // Return: (mixed) $data
227 | // Args: (mixed) $data
228 | public static function clean($data) {
229 | // String?
230 | if (!is_array($data)) return $data;
231 | // Needs a good scrubbing...
232 | $out = [];
233 | $whitelist = ['.priority'];
234 | // Recursive iterator to sanitize all keys (and flatten object values)
235 | foreach ($data as $key => $value) {
236 | $key = (in_array($key, $whitelist) !== false) ? $key : preg_replace('/[\.\#\$\/\[\]]/i', '', $key);
237 | if (is_object($value)) $value = $value->__toString();
238 | if (is_array($value) || is_object($value)) {
239 | $out[$key] = self::clean($value);
240 | } else {
241 | $out[$key] = $value;
242 | }
243 | }
244 | return $out;
245 | }
246 |
247 |
248 | // [STA]
249 | // Return: (string) Model ID
250 | // Args: (Model) $obj
251 | public static function getId($obj) {
252 | // Valid Eloquent Model Inheritance?
253 | if (method_exists($obj, 'getKey') && $key = $obj->getKey()) {
254 | if (!empty($key)) return $obj->getKey();
255 | }
256 | // Catch Generic
257 | if (isset($obj['id'])) return $obj['id'];
258 | if (isset($obj['_id'])) return $obj['_id'];
259 | if (isset($obj['$id'])) return $obj['$id'];
260 | // Else
261 | throw new \UnexpectedValueException('Invalid model object received: no primary ID (id, _id, $id)');
262 | }
263 |
264 | }
265 |
--------------------------------------------------------------------------------