├── 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 | --------------------------------------------------------------------------------