-
Notifications
You must be signed in to change notification settings - Fork 438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding a custom session handler with Cloud Datastore. #213
Changes from 19 commits
ab60a5a
f829d12
f63fe21
f91c494
d7bfb7f
4e1a99e
76fa9b4
a6a64b9
8da61b7
9c151a0
9e77624
c0dc2ef
a03c8e7
dc74e12
f105389
43761ad
9aaaa55
d3cb822
ca10b4e
e37da9d
021c628
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,303 @@ | ||
<?php | ||
/** | ||
* Copyright 2016 Google Inc. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
namespace Google\Cloud\Datastore; | ||
|
||
use Exception; | ||
use InvalidArgumentException; | ||
use SessionHandlerInterface; | ||
|
||
/** | ||
* Custom session handler backed by Cloud Datastore. | ||
* | ||
* {@see http://php.net/manual/en/class.sessionhandlerinterface.php} | ||
* | ||
* Instead of storing the session data in a local file, it stores the data to | ||
* Cloud Datastore. The biggest benefit of doing this is the data can be | ||
* shared by multiple instances, so it's suitable for cloud applications. | ||
* | ||
* The downside of using Cloud Datastore is the write operations will cost you | ||
* some money, so it is highly recommended to minimize the write operations | ||
* with your session data with this handler. In order to do so, keep the data | ||
* in the session as limited as possible; for example, it is ok to put only | ||
* signed-in state and the user id in the session with this handler. However, | ||
* for example, it is definitely not recommended that you store your | ||
* application's whole undo history in the session, because every user | ||
* operations will cause the Datastore write and then it will cost you lot of | ||
* money. | ||
* | ||
* This handler doesn't provide pessimistic lock for session data. Instead, it | ||
* uses Datastore Transaction for data consistency. This means that if | ||
* multiple requests are modifying the same session data simultaneously, there | ||
* will be more probablity that some of the `write` operations will fail. | ||
* | ||
* If you are building an ajax application which may issue multiple requests | ||
* to the server, please design the session data carefully, in order to avoid | ||
* possible data contentions. Also please see the 2nd example below for how to | ||
* properly handle errors on `write` operations. | ||
* | ||
* It uses the session.save_path as the Datastore namespace for isolating the | ||
* session data from your application data, it also uses the session.name as | ||
* the Datastore kind, the session id as the Datastore id. By default, it | ||
* does nothing on gc for reducing the cost. Pass positive value up to 1000 | ||
* for $gcLimit parameter to delete entities in gc. | ||
* | ||
* Note: The datastore transaction only lasts 60 seconds. If this handler is | ||
* used for long running requests, it will fail on `write`. | ||
* | ||
* Example without error handling: | ||
* ``` | ||
* use Google\Cloud\ServiceBuilder; | ||
* | ||
* $cloud = new ServiceBuilder(); | ||
* $datastore = $cloud->datastore(); | ||
* // or just $datastore = new DatastoreClient(); | ||
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
* | ||
* $handler = new DatastoreSessionHandler($datastore); | ||
* | ||
* session_set_save_handler($handler, true); | ||
* session_save_path('sessions'); | ||
* session_start(); | ||
* | ||
* // Then read and write the $_SESSION array. | ||
* | ||
* ``` | ||
* | ||
* The above example automatically writes the session data. It's handy, but | ||
* the code doesn't stop even if it fails to write the session data, because | ||
* the `write` happens when the code exits. If you want to know the session | ||
* data is correctly written to the Datastore, you need to call | ||
* `session_write_close()` explicitly and then handle `E_USER_WARNING` | ||
* properly like the following example. | ||
* | ||
* Example with error handling: | ||
* | ||
* ``` | ||
* use Google\Cloud\ServiceBuilder; | ||
* | ||
* $cloud = new ServiceBuilder(); | ||
* $datastore = $cloud->datastore(); | ||
* // or just $datastore = new DatastoreClient(); | ||
* | ||
* $handler = new DatastoreSessionHandler($datastore); | ||
* session_set_save_handler($handler, true); | ||
* session_save_path('sessions'); | ||
* session_start(); | ||
* | ||
* // Then read and write the $_SESSION array. | ||
* | ||
* function handle_session_error($errNo, $errStr, $errFile, $errLine) { | ||
* # We throw an exception here, but you can do whatever you need. | ||
* throw new Exception("$errStr in $errFile on line $errLine", $errNo); | ||
* } | ||
* set_error_handler('handle_session_error', E_USER_WARNING); | ||
* // If `write` fails for any reason, an exception will be thrown. | ||
* session_write_close(); | ||
* restore_error_handler(); | ||
* // You can still read the $_SESSION array after closing the session. | ||
* ``` | ||
*/ | ||
class DatastoreSessionHandler implements SessionHandlerInterface | ||
{ | ||
const DEFAULT_GC_LIMIT = 0; | ||
const DEFAULT_KIND = 'PHPSESSID'; | ||
const DEFAULT_NAMESPACE_ID = 'sessions'; | ||
/* | ||
* {@see https://cloud.google.com/datastore/docs/reference/rpc/google.datastore.v1#google.datastore.v1.PartitionId} | ||
*/ | ||
const NAMESPACE_ALLOWED_PATTERN = '/^[A-Za-z\d\.\-_]{0,100}$/'; | ||
const NAMESPACE_RESERVED_PATTERN = '/^__.*__$/'; | ||
|
||
/* @var int */ | ||
private $gcLimit; | ||
|
||
/* @var DatastoreClient */ | ||
private $datastore; | ||
|
||
/* @var string */ | ||
private $kind; | ||
|
||
/* @var string */ | ||
private $namespaceId; | ||
|
||
/* @var Transaction */ | ||
private $transaction; | ||
|
||
/** | ||
* Create a custom session handler backed by Cloud Datastore. | ||
* | ||
* @param DatastoreClient $datastore Datastore client. | ||
* @param int $gcLimit [optional] A number of entities to delete in the | ||
* garbage collection. Defaults to 0 which means it does nothing. | ||
* The value larger than 1000 will be cut down to 1000. | ||
*/ | ||
public function __construct( | ||
DatastoreClient $datastore, | ||
$gcLimit = self::DEFAULT_GC_LIMIT | ||
) { | ||
$this->datastore = $datastore; | ||
// Cut down to 1000 | ||
$this->gcLimit = min($gcLimit, 1000); | ||
$this->kind = self::DEFAULT_KIND; | ||
$this->namespaceId = self::DEFAULT_NAMESPACE_ID; | ||
} | ||
|
||
/** | ||
* Start a session, by creating a transaction for the later `write`. | ||
* | ||
* @param string $savePath The value of `session.save_path` setting will be | ||
This comment was marked as spam.
Sorry, something went wrong. |
||
* used here. It will use this value as the Datastore namespaceId. | ||
* @param string $sessionName The value of `session.name` setting will be | ||
* used here. It will use this value as the Datastore kind. | ||
* @return bool | ||
*/ | ||
public function open($savePath, $sessionName) | ||
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
{ | ||
$this->kind = $sessionName; | ||
if (preg_match(self::NAMESPACE_ALLOWED_PATTERN, $savePath) !== 1 || | ||
preg_match(self::NAMESPACE_RESERVED_PATTERN, $savePath) === 1) { | ||
throw new InvalidArgumentException( | ||
sprintf('The given save_path "%s" not allowed', $savePath) | ||
); | ||
} | ||
$this->namespaceId = $savePath; | ||
$this->transaction = $this->datastore->transaction(); | ||
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
return true; | ||
} | ||
|
||
/** | ||
* Just return true for this implementation. | ||
*/ | ||
public function close() | ||
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
{ | ||
return true; | ||
} | ||
|
||
/** | ||
* Read the session data from Cloud Datastore. | ||
*/ | ||
public function read($id) | ||
{ | ||
try { | ||
$key = $this->datastore->key( | ||
$this->kind, | ||
$id, | ||
['namespaceId' => $this->namespaceId] | ||
); | ||
$entity = $this->transaction->lookup($key); | ||
if ($entity !== null && isset($entity['data'])) { | ||
return $entity['data']; | ||
} | ||
} catch (Exception $e) { | ||
trigger_error( | ||
sprintf('Datastore lookup failed: %s', $e->getMessage()), | ||
E_USER_WARNING | ||
); | ||
} | ||
return ''; | ||
} | ||
|
||
/** | ||
* Write the session data to Cloud Datastore. | ||
*/ | ||
public function write($id, $data) | ||
{ | ||
try { | ||
$key = $this->datastore->key( | ||
$this->kind, | ||
$id, | ||
['namespaceId' => $this->namespaceId] | ||
); | ||
$entity = $this->datastore->entity( | ||
$key, | ||
[ | ||
'data' => $data, | ||
't' => time() | ||
] | ||
); | ||
$this->transaction->upsert($entity); | ||
$this->transaction->commit(); | ||
} catch (Exception $e) { | ||
trigger_error( | ||
sprintf('Datastore upsert failed: %s', $e->getMessage()), | ||
E_USER_WARNING | ||
); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* Delete the session data from Cloud Datastore. | ||
*/ | ||
public function destroy($id) | ||
{ | ||
try { | ||
$key = $this->datastore->key( | ||
$this->kind, | ||
$id, | ||
['namespaceId' => $this->namespaceId] | ||
); | ||
$this->transaction->delete($key); | ||
$this->transaction->commit(); | ||
} catch (Exception $e) { | ||
trigger_error( | ||
sprintf('Datastore delete failed: %s', $e->getMessage()), | ||
E_USER_WARNING | ||
); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* Delete the old session data from Cloud Datastore. | ||
*/ | ||
public function gc($maxlifetime) | ||
{ | ||
if ($this->gcLimit === 0) { | ||
return true; | ||
} | ||
try { | ||
$query = $this->datastore->query() | ||
->kind($this->kind) | ||
->filter('t', '<', time() - $maxlifetime) | ||
->order('t') | ||
->keysOnly() | ||
->limit($this->gcLimit); | ||
$result = $this->datastore->runQuery( | ||
$query, | ||
['namespaceId' => $this->namespaceId] | ||
); | ||
$keys = []; | ||
/* @var Entity $e */ | ||
foreach ($result as $e) { | ||
$keys[] = $e->key(); | ||
} | ||
if (!empty($keys)) { | ||
$this->datastore->deleteBatch($keys); | ||
} | ||
} catch (Exception $e) { | ||
trigger_error( | ||
sprintf('Session gc failed: %s', $e->getMessage()), | ||
E_USER_WARNING | ||
); | ||
return false; | ||
} | ||
return true; | ||
} | ||
} |
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.