Skip to content

Commit

Permalink
add ImmutableSession
Browse files Browse the repository at this point in the history
  • Loading branch information
drupol committed Apr 6, 2023
1 parent f27be64 commit fb42019
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 0 deletions.
181 changes: 181 additions & 0 deletions src/Storageless/Session/ImmutableSession.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license.
*/

declare(strict_types=1);

namespace PSR7Sessions\Storageless\Session;

use Exception;
use JsonSerializable;

use function array_key_exists;
use function json_decode;
use function json_encode;

use const JSON_PRESERVE_ZERO_FRACTION;
use const JSON_THROW_ON_ERROR;

final class ImmutableSession implements ImmutableSessionInterface
{
private const DEFAULT_JSON_DECODE_DEPTH = 512;

/**
* @param array<string, int|bool|string|float|mixed[]|null> $data
* @param array<string, int|bool|string|float|mixed[]|null> $originalData
*/
private function __construct(
private array $data,
private array $originalData,
) {
}

public static function fromDecodedTokenData(object $data): self
{
$arrayShapedData = self::convertValueToScalar($data);

return new self($arrayShapedData, $arrayShapedData);
}

/** @param array<int|bool|string|float|mixed[]|object|JsonSerializable|null> $data */
public static function fromTokenData(array $data): self
{
$instance = new self([], []);

foreach ($data as $key => $value) {
$instance->set((string) $key, $value);
}

$instance->originalData = $instance->data;

return $instance;
}

public static function newEmptySession(): self
{
return new self([], []);
}

/**
* {@inheritDoc}
*/
public function set(string $key, $value): void
{
throw new Exception('This session is immutable, use "withKeyValue" method instead.');
}

public function withKeyValue(string $key, mixed $value): ImmutableSessionInterface
{
$clone = clone $this;
$clone->data[$key] = self::convertValueToScalar($value);

return $clone;
}

public function withDecodedTokenData(object $data): ImmutableSessionInterface
{
$arrayShapedData = self::convertValueToScalar($data);

$clone = clone $this;
$clone->data = $arrayShapedData;
$clone->originalData = $arrayShapedData;

return $clone;
}

/**
* {@inheritDoc}
*/
public function get(string $key, $default = null)
{
if (! $this->has($key)) {
return self::convertValueToScalar($default);
}

return $this->data[$key];
}

public function remove(string $key): void
{
throw new Exception('This session is immutable, use "withoutKey" method instead.');
}

public function without(string $key): ImmutableSessionInterface
{
$clone = clone $this;

unset($clone->data[$key]);

return $clone;
}

public function clear(): void
{
throw new Exception('This session is immutable, use "withoutValues" method instead.');
}

public function withoutValues(): ImmutableSessionInterface
{
$clone = clone $this;
$clone->data = [];

return $clone;
}

public function has(string $key): bool
{
return array_key_exists($key, $this->data);
}

public function hasChanged(): bool
{
return $this->data !== $this->originalData;
}

public function isEmpty(): bool
{
return $this->data === [];
}

public function jsonSerialize(): object
{
return (object) $this->data;
}

/**
* @param int|bool|string|float|mixed[]|object|JsonSerializable|null $value
* @psalm-param ValueTypeWithObjects $value
*
* @return int|bool|string|float|mixed[]|null
* @psalm-return (ValueTypeWithObjects is object ? array<string, ValueType> : ValueType)
*
* @psalm-template ValueType of int|bool|string|float|array<mixed>|null
* @psalm-template ValueTypeWithObjects of ValueType|object
*/
private static function convertValueToScalar(int|bool|string|float|array|object|null $value): int|bool|string|float|array|null
{
/** @psalm-var ValueType $decoded */
$decoded = json_decode(
json_encode($value, JSON_PRESERVE_ZERO_FRACTION | JSON_THROW_ON_ERROR),
true,
self::DEFAULT_JSON_DECODE_DEPTH,
JSON_THROW_ON_ERROR,
);

return $decoded;
}
}
14 changes: 14 additions & 0 deletions src/Storageless/Session/ImmutableSessionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace PSR7Sessions\Storageless\Session;

interface ImmutableSessionInterface extends SessionInterface
{
public function withKeyValue(string $key, mixed $value): ImmutableSessionInterface;

public function without(string $key): ImmutableSessionInterface;

public function withoutValues(): ImmutableSessionInterface;
}

0 comments on commit fb42019

Please sign in to comment.