Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
parameters:
treatPhpDocTypesAsCertain: false
level: 6
level: 7
paths:
- src
- test
Expand Down
14 changes: 11 additions & 3 deletions src/ArrayHelpers/AssociativeArrayUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

namespace Ingenerator\PHPUtils\ArrayHelpers;

use InvalidArgumentException;

use function array_key_exists;
use function array_keys;
use function array_shift;
Expand Down Expand Up @@ -120,12 +122,18 @@ public static function pathsToNested(array $input): array
*/
public static function setPath(array &$array, string|array $path, mixed $value, string $delimiter = '.'): void
{
// The path has already been separated into keys
$keys = $path;
if (!is_array($path)) {
if (is_array($path)) {
$keys = $path;
} else {
if ($delimiter === '') {
throw new InvalidArgumentException('Path delimiter cannot be empty');
}
// Split the keys by delimiter
$keys = explode($delimiter, $path);
}
if ($keys === []) {
throw new InvalidArgumentException('Path must not be empty');
}

// Set current $array to inner-most array path
while (count($keys) > 1) {
Expand Down
10 changes: 7 additions & 3 deletions src/CSV/CSVWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ public function open(mixed $file, array $options = []): void
$this->resource = $file;
$this->owns_resource = false;
} elseif (is_string($file)) {
$this->resource = fopen($file, 'w');
$resource = fopen($file, 'w');
if ($resource === false) {
throw new RuntimeException('Unable to open CSV file '.$file);
}
$this->resource = $resource;
$this->owns_resource = true;
} else {
throw new InvalidArgumentException(
Expand Down Expand Up @@ -104,7 +108,7 @@ public function close(): void
}

/**
* @param int[]|string[] $keys
* @param list<array-key> $keys
*/
private function writeHeaders(array $keys): void
{
Expand All @@ -113,7 +117,7 @@ private function writeHeaders(array $keys): void
} else {
array_walk(
$keys,
fn (string $str): true => !str_contains($str, ',')
fn (int|string $str): true => !str_contains((string) $str, ',')
?: throw new RuntimeException(
"Column header `$str` cannot contain comma if headers are not quoted"
)
Expand Down
13 changes: 12 additions & 1 deletion src/DateTime/DateTimeImmutableFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public static function atMicrotime(float|string $microtime): DateTimeImmutable
// First, a float with a .00 casts to string with no decimal point, which causes the createFromFormat to fail
// on exact seconds. Use sprintf to ensure there's always a decimal present
$dt = DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', $microtime));
if (!$dt instanceof DateTimeImmutable) {
throw new InvalidArgumentException(sprintf('Could not parse microtime value %s', $microtime));
}

// Second, creating from unix timestamp of any kind always sets the TZ to UTC (*even* if you specify a timezone
// in the constructor).
Expand Down Expand Up @@ -95,6 +98,10 @@ public static function fromIso(string $value): DateTimeImmutable
{
// Cope with Z for Zulu time instead of +00:00 - PHP offers `p` for this, but that then doesn't accept '+00:00'
$fixed_value = preg_replace('/Z/i', '+00:00', $value);
if ($fixed_value === null) {
// regex failed
throw new InvalidArgumentException('Failed to replace text in iso string');
}

// Pad / truncate milliseconds to 6 digits as that's the precision PHP can support
// Regex is a bit dull here, but we need to be sure we can reliably find the (possibly absent)
Expand All @@ -111,10 +118,14 @@ public static function fromIso(string $value): DateTimeImmutable
.$matches['tz_prefix'],
$fixed_value
);
if ($fixed_value === null) {
// regex failed
throw new InvalidArgumentException("Cannot replace parts in `$value` ISO string");
}

// Not using fromStrictFormat as I want to throw with the original value, not the parsed value
$date = DateTimeImmutable::createFromFormat('!Y-m-d\TH:i:s.uP', $fixed_value);
if (DateString::isoMS($date ?: null) === $fixed_value) {
if ($date instanceof DateTimeImmutable && DateString::isoMS($date) === $fixed_value) {
return $date;
}
throw new InvalidArgumentException("`$value` cannot be parsed as a valid ISO date-time");
Expand Down
17 changes: 14 additions & 3 deletions src/DeploymentConfig/ConfigMapDeclarationParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Ingenerator\PHPUtils\DeploymentConfig;

use function array_values;

class ConfigMapDeclarationParser
{
/**
Expand Down Expand Up @@ -60,10 +62,19 @@ protected function validateDeclaration(mixed $declaration): array
throw InvalidConfigException::invalidMapDeclaration('must be indexed array with keys 0, 1');
}

if (is_string($declaration[0])) {
$declaration[0] = [$declaration[0]];
$environments = $declaration[0];
if (is_string($environments)) {
$environments = [$environments];
}
if (!is_array($environments)) {
throw InvalidConfigException::invalidMapDeclaration('environments must be a string or array of strings');
}
foreach ($environments as $environment) {
if (!is_string($environment)) {
throw InvalidConfigException::invalidMapDeclaration('environments must be a string or array of strings');
}
}

return $declaration;
return [array_values($environments), $declaration[1]];
}
}
6 changes: 5 additions & 1 deletion src/DeploymentConfig/ConfigValueDecrypter.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ protected function loadAndValidateKeypair(string $name): string
if (!is_file($keypair_file)) {
throw InvalidConfigException::missingKeypair($keypair_file);
}
$keypair = base64_decode(file_get_contents($keypair_file), true);
$keypair_content = file_get_contents($keypair_file);
if ($keypair_content === false) {
throw InvalidConfigException::corruptKeypair($keypair_file, 'could not read file');
}
$keypair = base64_decode($keypair_content, true);
if ($keypair === false) {
throw InvalidConfigException::corruptKeypair($keypair_file, 'invalid base64');
}
Expand Down
7 changes: 6 additions & 1 deletion src/Encryption/CryptoBox/CryptoBoxPublicKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ public static function fromString(string $exported): static
throw new InvalidArgumentException('Invalid public key string format');
}

$public_key = Base64Url::decode($matches['key_b64']);
if ($public_key === false) {
throw new InvalidArgumentException('Invalid public key encoding');
}

return new self(
public_key: Base64Url::decode($matches['key_b64']),
public_key: $public_key,
// public key is not secret, no need for constant-time encoding
keypair_id: $matches['keypair_id']
);
Expand Down
7 changes: 6 additions & 1 deletion src/Encryption/CryptoBox/CryptoBoxString.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ public static function fromString(string $exported): static
throw new InvalidArgumentException('Invalid ciphertext string format');
}

$ciphertext = Base64Url::decode($matches['ciphertext_b64']);
if ($ciphertext === false) {
throw new InvalidArgumentException('Invalid ciphertext encoding');
}

return new self(
ciphertext: Base64Url::decode($matches['ciphertext_b64']),
ciphertext: $ciphertext,
// ciphertext is not secret, no need for constant-time encoding
keypair_id: $matches['keypair_id']
);
Expand Down
2 changes: 1 addition & 1 deletion src/Logging/DefaultLogMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static function httpContext(array $server): array
*
* @param array<string, mixed> $server
*
* @return array<string, string|array<string, string>>
* @return array{context: array{req: string}, 'logging.googleapis.com/trace': string}
*/
public static function requestTrace(array $server = [], ?string $gcp_project = null): array
{
Expand Down
11 changes: 6 additions & 5 deletions src/Logging/StackdriverApplicationLogger.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Throwable;

use function array_pad;
use function array_values;
use function debug_backtrace;
use function file_put_contents;
use function http_response_code;
Expand Down Expand Up @@ -115,7 +116,7 @@ public function __construct(
protected string $log_destination,
array|callable|LogMetadataProvider ...$metadata_sources,
) {
$this->metadata_sources = array_pad($metadata_sources, 2, []);
$this->metadata_sources = array_values(array_pad($metadata_sources, 2, []));
$this->call_site_finder = new ExternalCallSiteFinder();
}

Expand Down Expand Up @@ -283,7 +284,7 @@ protected function formatException(Throwable $e): array
* This method renders a string trace that follows the format of the standard trace (so it can be
* parsed by tools that are looking for that) but without including any function argument details.
*
* @param array<int, array{file?: string, line?: int, class: string, type: string, function: string }> $trace_stack
* @param list<array{file?: string, line?: int, class?: string, type?: string, function?: string}> $trace_stack
*/
protected function getExceptionTraceAsStringWithoutArgs(array $trace_stack): string
{
Expand All @@ -293,10 +294,10 @@ protected function getExceptionTraceAsStringWithoutArgs(array $trace_stack): str
$trace_lines[] = sprintf(
'#%d %s: %s%s%s()',
$index,
isset($trace['file']) ? $trace['file'].'('.$trace['line'].')' : '[internal function]',
isset($trace['file'], $trace['line']) ? $trace['file'].'('.$trace['line'].')' : '[internal function]',
$trace['class'] ?? '',
$trace['type'] ?? '',
$trace['function']
$trace['function'] ?? ''
);
}
$trace_lines[] = '#'.count($trace_stack).' {main}';
Expand Down Expand Up @@ -382,7 +383,7 @@ public function logRequest(?array $server, ?float $request_start_time = null): v
$meta = $this->getMetadata();
$log_entry = AssociativeArrayUtils::deepMerge(
[
'severity' => strtoupper($this->getLogPriorityForHttpCode($http_code)),
'severity' => strtoupper($this->getLogPriorityForHttpCode((string) $http_code)),
self::PROP_INGEN_TYPE => 'rqst',
'httpRequest' => $meta['context']['httpRequest'] ?? [],
'context' => [
Expand Down
21 changes: 17 additions & 4 deletions src/Mutex/DbBackedMutexWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Ingenerator\PHPUtils\Mutex;

use PDO;
use PDOStatement;
use RuntimeException;

class DbBackedMutexWrapper implements MutexWrapper
{
Expand All @@ -25,9 +27,8 @@ protected function getLock(string $name, int $timeout_seconds): void
{
// Note: These are borrowed from and could be used for our MysqlSession class
$name_param = $this->db->quote($name);
$result = $this->db
->query("SELECT GET_LOCK($name_param, $timeout_seconds)")
->fetchAll(PDO::FETCH_COLUMN, 0);
$query = $this->runQuery("SELECT GET_LOCK($name_param, $timeout_seconds)");
$result = $query->fetchAll(PDO::FETCH_COLUMN, 0);

// Need to explicitly cast the result to an int for comparison as PDO value types vary between 8.0 and 8.1+
// And we should keep this cast even when we drop 8.0, because the PDO int/string mode is actually
Expand All @@ -40,6 +41,18 @@ protected function getLock(string $name, int $timeout_seconds): void
protected function releaseLock(string $name): void
{
$name_param = $this->db->quote($name);
$this->db->query("SELECT RELEASE_LOCK($name_param)")->fetchAll();
$query = $this->runQuery("SELECT RELEASE_LOCK($name_param)");
$query->fetchAll();
}

private function runQuery(string $str): PDOStatement
{
$query = $this->db->query($str);
if ($query === false) {
// If ATTR_ERRMODE was not set to PDO::ERRMODE_EXCEPTION, the method returns false rather than throwing,
throw new RuntimeException('Failed to execute query'.json_encode($this->db->errorInfo()));
}

return $query;
}
}
2 changes: 2 additions & 0 deletions src/Object/ConstantDirectory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ final class ConstantDirectory
*/
private array $constants;

/** @param class-string $class_name */
private function __construct(private readonly string $class_name)
{
}

/** @param class-string $class */
public static function forClass(string $class): static
{
return new self($class);
Expand Down
13 changes: 9 additions & 4 deletions src/Object/ObjectPropertyRipper.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use Closure;
use DomainException;
use LogicException;
use stdClass;

use function get_object_vars;
Expand All @@ -34,7 +35,8 @@ class ObjectPropertyRipper
*/
public static function rip(object $object, array $properties): array
{
$ripper = static::getRipper($object::class);
$class = $object::class;
$ripper = static::getRipper($class);

return $ripper($object, $properties);
}
Expand Down Expand Up @@ -83,6 +85,8 @@ public static function ripOne(object $object, string $property): mixed
/**
* Create a callback bound to the scope of the provided class so that it has access to internal
* properties.
*
* @param class-string $class
*/
protected static function getRipper(string $class): Closure
{
Expand All @@ -102,9 +106,9 @@ function ($object, $properties): array {
}

/**
* @param object|class-string<object> $scope
* @param object|class-string $scope
*/
private static function bindScopedClosure(callable $callback, object|string $scope): Closure
private static function bindScopedClosure(Closure $callback, object|string $scope): Closure
{
$scope_class = is_object($scope) ? $scope::class : $scope;
if ($scope_class === stdClass::class) {
Expand All @@ -115,6 +119,7 @@ private static function bindScopedClosure(callable $callback, object|string $sco
$scope = null;
}

return Closure::bind($callback, newThis: null, newScope: $scope);
return Closure::bind($callback, newThis: null, newScope: $scope)
?? throw new LogicException('Unable to bind closure to requested scope');
}
}
9 changes: 6 additions & 3 deletions src/Object/ScopeChangingCaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Ingenerator\PHPUtils\Object;

use Closure;
use LogicException;

class ScopeChangingCaller
{
Expand All @@ -27,12 +28,14 @@ class ScopeChangingCaller
* @template TReturn of mixed
* @template TArgs
*
* @param callable(TArgs):TReturn $func
* @param class-string $class
* @param Closure(TArgs):TReturn $func
* @param TArgs $args,...
*/
public static function call(string $class, callable $func, mixed ...$args): mixed
public static function call(string $class, Closure $func, mixed ...$args): mixed
{
$func = Closure::bind($func, null, $class);
$func = Closure::bind($func, null, $class)
?? throw new LogicException('Unable to bind closure to requested scope');

return $func(...$args);
}
Expand Down
9 changes: 7 additions & 2 deletions src/Random/ConsistentStringScrambler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use InvalidArgumentException;
use Random\Engine\Mt19937;
use Random\Randomizer;
use RuntimeException;

use function array_filter;
use function array_values;
Expand All @@ -24,7 +25,11 @@ public function shuffleWords(?string $input, string $random_seed): ?string
}

// Break the string into words (and return null if there is no content / only whitespace)
$words = array_filter(preg_split('/\s+/', $input ?? ''));
$parts = preg_split('/\s+/', $input ?? '');
if ($parts === false) {
throw new RuntimeException('Unable to split input into words');
}
$words = array_values(array_filter($parts));

if ($words === []) {
return null;
Expand Down Expand Up @@ -60,6 +65,6 @@ private function seededShuffle(array $items, int $seed): array
$items[$rnd] = $old_item_i;
}

return $items;
return array_values($items);
}
}
Loading
Loading