May 2017

Thursday, May 25, 2017

What is the best way to use DomainSession in a command line environment?

Jory Geerts asks: #

Do you have any thoughts on how [DomainSession] could be used from a CLI environment? #

The first thing to ask yourself is how would you like sessions to work on the command line? There are a few ways this could work. #

Each run has its own new session #

The main reason I see for this option would be to reuse the domain logic from your web app and it depends on DomainSession. You don’t really care what gets stored in the session because you consider it temporary. #

In bin/with_new.php I’ve defined a very simple script. #

First, I init the script. In this case, since I’m using a Radar application, I just use Aura.Di directly to load DomainModule. This makes sure I have SessionManager and Home configured. #

If I was doing this in production, I’d probably have a different definition for DomainModule that I used with the CLI that used the Memory storage instead of File storage. In this example, I won’t worry about that. #

Then I can get a new instance of my domain object Home and call it with null as the $sessionId. This will let the SessionManager generate a new ID for this request. #

$home = $di->newInstance(Home::class);
$payload = $home(null);

Each run has its own new prepopulated session #

If I need to prepopulate the session with data (like a logged in user) I can simply start a new session, assign the values I want to the session and finish it like normal. Then I can grab the session ID from that session and pass it into the domain object so it can reuse that session. #

I implement this in bin/with_prepopulated.php. #

$sessionManager = $di->newInstance(SessionManager::class);
$session = $sessionManager->start(null);
$session->timestamp = '1981-08-27 17:00:00';
$sessionManager->finish($session);
$sessionId = $session->getId()->value();
$home = $di->newInstance(Home::class);
$payload = $home($sessionId);

Reuse the session between script runs #

The next option would be if you wanted to reuse the session between script runs. You’ll need to determine how you want to store the $sessionId between script runs, but one way would be to print it in the output, then include it as a parameter when calling your script in the future. #

I implement this in bin/with_reused.php. #

$sessionId = isset($argv[1]) ? $argv[1] : null;
$home = $di->newInstance(Home::class);
$payload = $home($sessionId);
echo "Timestamp: {$payload['timestamp']}\n";
echo "Session ID: {$payload['session']->getId()->value()}\n";

The output would work as such: #

$ php with_reused.php
Timestamp: Unknown
Session ID: 2337858b313a2e4fb3bc2fd8b0636e5a
$ php with_reused.php 2337858b313a2e4fb3bc2fd8b0636e5a
Timestamp: 2017-05-25 16:32:35
Session ID: 2337858b313a2e4fb3bc2fd8b0636e5a
$ php with_reused.php 2337858b313a2e4fb3bc2fd8b0636e5a
Timestamp: 2017-05-25 16:32:44
Session ID: 2337858b313a2e4fb3bc2fd8b0636e5a

Summary #

These three ways illustrate some of the ways you could to go about solving this issue. There will most likely be specific requirements based on your application, but it will most likely be some variation of these. For instance instead of printing the $sessionId you might store it in a file. #

What do you think? Let me know in the comments if you see any problems with these approaches. #

Tuesday, May 2, 2017

What is the best way to handle sessions with ADR?

Right before Christmas, I reached out to Paul Jones asking: #

I’ve reached a point with a couple of my Radar projects where I need to add a login and set permissions. I’m trying to figure out the best way to handle this, especially with PSR-7 and ADR. #

This led to discussing how to do sessions properly in Radar. I had read Paul’s post PSR-7 and Session Cookies which discussed how to tweak PHP Sessions to work better in a PSR-7 architecture. You don’t really want PHP automatically sending and reading cookies outside of the Request object. #

Paul responded: #

Finally, the more I work with separated domain layer, the more I dislike the built-in PHP session system. Lately, I have started to consider avoiding it entirely, in favor of something like a custom DomainSession. Attached are my very rough notes-in-code; maybe they’ll be useful to you. #

Needless to say, they have been very useful. Paul has been incredibly generous with his time and after going back and forth through several rounds of refactoring and revising I’m ready to officially announce Cadre.DomainSession. The current version as of this post is 0.4.0. #

Why DomainSession? #

The reason for this package is that as Paul had said: “Anything that touches storage should be considered domain-layer work.” so it’s ok to read the session cookie in an Input class, and it’s ok to write the cookie in a Responder class, but pretty much everything else should be in the Domain layer. #

For more information about Radar see Radar Under the Hood. #

What does it do? #

Cadre.DomainSession takes a session id (or generates one) and loads session data from storage. It’s smart enough to handle regenerating session ids and cleaning up expired sessions. #

Demo #

I’ve put together a small demo app using Radar and Cadre.DomainSession at radar-domain-session. #

Loading the Session ID From a Cookie #

Reading the session ID from a cookie is handled in the Input of your route. I used the excellent library dflydev-fig-cookies to simplify getting and setting cookies in PSR-7. #

In Application\Delivery\DefaultInput I read from the SESSION_ID cookie, returning null if it doesn’t exist. #

public function __invoke(Request $request) {
	$sessionId = FigRequestCookies::get($request, 'SESSION_ID');
	return [$sessionId->getValue()];
}
Starting, Using and Finishing the Session #

In Application\Domain\Home I first inject the Cadre\DomainSession\SessionManager. Then I start the session with the session ID I passed in from the Input. I check to see if there is a timestamp session value (Unknown if not present) and assign it with the current timestamp. Finally, I finish the session which is what persists the data to storage. In this case, it’s just to the filesystem. One important thing to note is that I have to return the session object in the payload for the final step. #

public function __construct(SessionManager $sessionManager) {
	$this->sessionManager = $sessionManager;
}
public function __invoke($sessionId) {
	$session = $this->sessionManager->start($sessionId);
	$lastTimestamp = $session->timestamp ?? 'Unknown';
	$session->timestamp = date('Y-m-d H:i:s');
	$this->sessionManager->finish($session);
	return [
		'success' => true,
		'session' => $session,
		'timestamp' => $lastTimestamp,
	];
}

If you want to regenerate the session ID you can do so by calling $session->getId()->regenerate(). #

Persisting the Session ID in a Cookie #

In Application\Delivery\DefaultResponder I check to see if there was a session in the payload. If there is, I check to see if the session ID has been updated (new or regenerated session id). If it’s been updated I persist the session ID value to the session cookie I’m reading from in the Input and that’s all there is to it. #

if (isset($payload['session']) && $payload['session'] instanceof Session) {
	if ($payload['session']->getId()->hasUpdatedValue()) {
		$this->response = FigResponseCookies::set(
			$this->response,
			SetCookie::create('SESSION_ID')
				->withValue($payload['session']->getId()->value())
		);
	}
}

Concerns #

I have some concerns with this library. The built-in session handling has withstood the test of time. It’s been looked at and I assume many security concerns have been fixed. I’m not a security expert, so I worry that there are vulnerabilities in my code. #

In particular, I’m using what I consider to be a fairly naive method for generating session IDs. #

public function regenerate(int $bytes = 16): string {
	$this->value = bin2hex(random_bytes($bytes));
	return $this->value;
}

I need to do some research and find if session_create_id would be a better method to use. I’m not sure if it just generates an ID or if it depends on the storage implementation. #

Summary #

When you’re using ADR, it’s important to be absolutely clear what is and isn’t part of your domain. Paul considers anything that touches storage as domain-layer work. Reading and writing cookies is an implementation detail that is part of the delivery mechanism. Imagine using the same domain objects in a command line application. You wouldn’t use cookies, but perhaps you would pass a session ID in as a command line argument. #

UPDATE: #

There is also an interesting conversation going over on Reddit. #

Published by Andrew Shell on and last updated .