Skip to content

bakame-php/http-structured-fields

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HTTP Structured Fields for PHP

Author Software License Build Latest Version Total Downloads Sponsor development of this project

bakame/http-structured-fields is a framework-agnostic PHP library that allows you to parse, serialize create, update and validate HTTP Structured Fields in PHP according to the RFC9651.

Once installed you will be able to do the following:

use Bakame\Http\StructuredFields\DataType;
use Bakame\Http\StructuredFields\Token;

//1 - parsing an Accept Header
$fieldValue = 'text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8';
$field = DataType::List->fromRfc9651($fieldValue);
$field[2]->value()->toString(); // returns 'application/xml'
$field[2]->parameter('q');      // returns (float) 0.9
$field[0]->value(
    fn (mixed $value) => $value instanceof Token && str_contains($value->toString(), '/')
)->toString(); // returns 'text/html' or throw a ValidationError exception in case of error
$field[0]->parameter(key: 'q', default: 1.0); // returns 1.0 if the parameter is not defined

System Requirements

PHP >= 8.1 is required but the latest stable version of PHP is recommended.

Installation

Use composer:

composer require bakame/http-structured-fields

Documentation

Warning

The documentation for v2 is still not fully finished please refers to version 1.x for the most recent and stable documentation.

The package is compliant against RFC9651 as such it exposes all the data type and all the methods expected to comply with the RFC requirements

Basic Usage

Structured Fields are a set of rules established to allow for easier and more predicable HTTP header or trailer process. The package abstracts this complexity and define useful features to enable better validation.

While The RFC defines 5 (five) structured data type, you can access then using the DataType enum for a quick usage.

$headerLine = 'bar;baz=42'; //the raw header line is a structured field item
$field = DataType::Item->fromHttpValue($headerLine, Ietf::Rfc8941); // parse the field using RFC8941
$field->value()->toString(); //returns bar
$field->parameter('baz'); //return 42 as an integer

$field->toHttpValue(Ietf::Rfc9651); // serialize the field using RFC9651
$field->toHttpValue(); // serialize the field using RFC9651 as it is the latest stable specification
echo $field;           // serialize the field using RFC9651 as it is the latest stable specification

Because there are 2 RFCs (RFC8941 which is now obsolete and superseded by RFC9651), the package provides an Enum called Ietf to help you choose which RFC you need during serialization and parsing. If the Enum is not used the package will fallback to using the latest stable RFC. (ie: RFC9651).

To simplify your code, your can use this alternative syntax:

$headerLine = 'bar;baz=42'; //the raw header line is a structured field item
$field = DataType::Item->fromRFC8941($headerLine); // parses the field using RFC8941
$field->toRfc9651(); // serializes the field using RFC9651
$field->toRfc8941(); // serializes the field using RFC8941

Warning

If parsing or serializing is not possible, a SyntaxError exception is thrown with the information about why the conversion could not be achieved.

At any given moment when building your structured field you can use the Ietf::supports method to know whether your current work is valid against any of the RFC.

$item = DataType::Item->fromRFC9651('@123456789');
Ietf::Rfc8941->supports($item); //returns false - the date type does not exist in RFC8941
Ietf::Rfc9651->supports($item); //returns true

Accessing Structured Fields Values

The package provides methods to access the field values and convert them to PHP type whenever possible.The table below summarizes the value type.

RFC Type PHP Type Package Enum Name Package Enum Value RFC min. version
Integer int Type::Integer ìnteger RFC8941
Decimal float Type::Decimal decimal RFC8941
String string Type::String string RFC8941
Boolean bool Type::Boolean boolean RFC8941
Token class Token Type::Token token RFC8941
Byte Sequence class ByteSequence Type::ByteSequence binary RFC8941
Date class DateTimeImmutable Type::Date date RFC9651
DisplayString class DisplayString Type::DisplayString displaystring RFC9651

Warning

The translation to PHP native type does not mean that all PHP values are usable. for instance not all integer are valid integer for structured fields and not all string are valid string according to the RFCs.

$headerLine = 'bar;baz=42'; //the raw header line is a structured field item
$field = DataType::Item->fromRFC8941($headerLine); // parses the field using RFC8941
$field->value(); // returns Token::fromString('bar');
$field->parameter('baz'); // returns (int) 42
$field->parameterByIdex(0); //returns ['baz' => 42];

For headers or trailer when the index is important the package allows selecting parameters by name or by index.

Building and Updating Structured Fields Values

Every value object can be used as a builder to create an HTTP field value. Because we are using immutable value objects any change to the value object will return a new instance with the changes applied and leave the original instance unchanged.

Items value

The Item value object exposes the following named constructors to instantiate bare items (ie: item without parameters attached to them).

use Bakame\Http\StructuredFields\ByteSequence;
use Bakame\Http\StructuredFields\Item;
use Bakame\Http\StructuredFields\Token;

Item:new(DateTimeInterface|ByteSequence|Token|DisplayString|string|int|array|float|bool $value): self
Item::fromDecodedByteSequence(Stringable|string $value): self;
Item::fromEncodedDisplayString(Stringable|string $value): self;
Item::fromDecodedDisplayString(Stringable|string $value): self;
Item::fromEncodedByteSequence(Stringable|string $value): self;
Item::fromToken(Stringable|string $value): self;
Item::fromString(Stringable|string $value): self;
Item::fromDate(DateTimeInterface $datetime): self;
Item::fromDateFormat(string $format, string $datetime): self;
Item::fromDateString(string $datetime, DateTimeZone|string|null $timezone = null): self;
Item::fromTimestamp(int $value): self;
Item::fromDecimal(int|float $value): self;
Item::fromInteger(int|float $value): self;
Item::true(): self;
Item::false(): self;

To update the Item instance value, use the withValue method:

use Bakame\Http\StructuredFields\Item;

Item::withValue(DateTimeInterface|ByteSequence|Token|DisplayString|string|int|float|bool $value): static

Ordered Maps

The Dictionary and Parameters are ordered map instances. They can be built using their keys with an associative iterable structure as shown below

use Bakame\Http\StructuredFields\Dictionary;

$value = Dictionary::fromAssociative([
    'b' => Item::false(),
    'a' => Item::fromToken('bar'),
    'c' => new DateTimeImmutable('2022-12-23 13:00:23'),
]);

echo $value->toHttpValue(); //"b=?0, a=bar, c=@1671800423"
echo $value;                //"b=?0, a=bar, c=@1671800423"

or using their indexes with an iterable structure of pairs (tuple) as defined in the RFC:

use Bakame\Http\StructuredFields\Parameters;
use Bakame\Http\StructuredFields\Item;

$value = Parameters::fromPairs(new ArrayIterator([
    ['b', Item::false()],
    ['a', Item::fromToken('bar')],
    ['c', new DateTime('2022-12-23 13:00:23')]
]));

echo $value->toHttpValue(); //;b=?0;a=bar;c=@1671800423
echo $value;                //;b=?0;a=bar;c=@1671800423

If the preference is to use the builder pattern, the same result can be achieved with the following steps:

  • First create a Parameters or a Dictionary instance using the new named constructor which returns a new instance with no members.
  • And then, use any of the following modifying methods to populate it.
$map->add(string $key, $value): static;
$map->append(string $key, $value): static;
$map->prepend(string $key, $value): static;
$map->mergeAssociative(...$others): static;
$map->mergePairs(...$others): static;
$map->remove(string|int ...$key): static;

As shown below: `

use Bakame\Http\StructuredFields\Dictionary;
use Bakame\Http\StructuredFields\Item;
use Bakame\Http\StructuredFields\Token;

$value = Dictionary::new()
    ->add('a', InnerList::new(
        Item::fromToken('bar'),
        Item::fromString('42'),
        Item::fromInteger(42),
        Item::fromDecimal(42)
     ))
    ->prepend('b', Item::false())
    ->append('c', Item::fromDateString('2022-12-23 13:00:23'))
;

echo $value->toHttpValue(); //b=?0, a=(bar "42" 42 42.0), c=@1671800423
echo $value;                //b=?0, a=(bar "42" 42 42.0), c=@1671800423

It is possible to also build Dictionary and Parameters instances using indexes and pair as per described in the RFC.

The $pair parameter is a tuple (ie: an array as list with exactly two members) where:

  • the first array member is the parameter $key
  • the second array member is the parameter $value
$map->unshift(array ...$pairs): static;
$map->push(array ...$pairs): static;
$map->insert(int $key, array ...$pairs): static;
$map->replace(int $key, array $pair): static;
$map->removeByKeys(string ...$keys): static;
$map->removeByIndices(int ...$indices): static;

We can rewrite the previous example

use Bakame\Http\StructuredFields\Dictionary;
use Bakame\Http\StructuredFields\Item;
use Bakame\Http\StructuredFields\Token;

$value = Dictionary::new()
    ->push(
        ['a', InnerList::new(
            Item::fromToken('bar'),
            Item::fromString('42'),
            Item::fromInteger(42),
            Item::fromDecimal(42)
         )],
         ['c', Item::true()]
     )
    ->unshift(['b', Item::false()])
    ->replace(2, ['c', Item::fromDateString('2022-12-23 13:00:23')])
;

echo $value->toHttpValue(); //b=?0, a=(bar "42" 42 42.0), c=@1671800423
echo $value;                //b=?0, a=(bar "42" 42 42.0), c=@1671800423

Caution

on duplicate keys pair values are merged as per RFC logic.

The remove always accepted string or integer as input.

$field = Dictionary::fromHttpValue('b=?0, a=(bar "42" 42 42.0), c=@1671800423');
echo $field->remove('b', 2)->toHttpValue(); // returns a=(bar "42" 42 42.0)

If a stricter approach is needed, use the following new methods removeByIndices and/or removeByKeys:

use Bakame\Http\StructuredFields\Parameters;

$field = Parameters::fromHttpValue(';expire=@1681504328;path="/";max-age=2500;secure;httponly=?0;samesite=lax');
echo $field->removeByIndices(4, 2, 0)->toHttpValue();                      // returns ;path="/";secure;samesite=lax
echo $field->removeByKeys('expire', 'httponly', 'max-age')->toHttpValue(); // returns ;path="/";secure;samesite=lax

Automatic conversion

For all containers, to ease instantiation the following automatic conversion are applied on the member argument of each modifying methods.

If the submitted type is:

  • a StructuredField implementing object, it will be passed as is
  • an iterable structure, it will be converted to an InnerList instance using InnerList::new
  • otherwise, it is converted into an Item using the Item::new named constructor.

If no conversion is possible an InvalidArgument exception will be thrown.

This means that both constructs below built equal objects

use Bakame\Http\StructuredFields\Dictionary;
use Bakame\Http\StructuredFields\Item;
use Bakame\Http\StructuredFields\Token;

echo Dictionary::new()
    ->add('a', InnerList::new(
        Item::fromToken('bar'),
        Item::fromString('42'),
        Item::fromInteger(42),
        Item::fromDecimal(42)
     ))
    ->prepend('b', Item::false())
    ->append('c', Item::fromDateString('2022-12-23 13:00:23'))
    ->toHttpValue()
;

echo Dictionary::new()
    ->add('a', [Token::fromString('bar'), '42', 42, 42.0])
    ->prepend('b', false)
    ->append('c', new DateTimeImmutable('2022-12-23 13:00:23'))
    ->toHttpValue()
;

 // both will return 'b=?0, a=(bar "42" 42 42.0), c=@1671800423

Of course, it is possible to mix both notations.

Lists

To create OuterList and InnerList instances you can use the new named constructor which takes a single variadic parameter $members:

use Bakame\Http\StructuredFields\InnerList;
use Bakame\Http\StructuredFields\ByteSequence;

$list = InnerList::new(
    ByteSequence::fromDecoded('Hello World'),
    42.0,
    42
);

echo $list->toHttpValue(); //'(:SGVsbG8gV29ybGQ=: 42.0 42)'
echo $list;                //'(:SGVsbG8gV29ybGQ=: 42.0 42)'

Once again, the builder pattern can be used via a combination of the new named constructor and the use any of the following modifying methods.

$list->unshift(...$members): static;
$list->push(...$members): static;
$list->insert(int $key, ...$members): static;
$list->replace(int $key, $member): static;
$list->remove(int ...$key): static;

as shown below

use Bakame\Http\StructuredFields\ByteSequence;
use Bakame\Http\StructuredFields\InnerList;

$list = InnerList::new()
    ->unshift('42')
    ->push(42)
    ->insert(1, 42.0)
    ->replace(0, ByteSequence::fromDecoded('Hello World'));

echo $list->toHttpValue(); //'(:SGVsbG8gV29ybGQ=: 42.0 42)'
echo $list;                //'(:SGVsbG8gV29ybGQ=: 42.0 42)'

It is also possible to create an OuterList based on an iterable structure of pairs.

use Bakame\Http\StructuredFields\OuterList;

$list = OuterList::fromPairs([
    [
        ['foo', 'bar'],
        [
            ['expire', new DateTime('2024-01-01 12:33:45')],
            ['path', '/'],
            [ 'max-age', 2500],
            ['secure', true],
            ['httponly', true],
            ['samesite', Token::fromString('lax')],
        ]
    ],
    [
        'coucoulesamis', 
        [['a', false]],
    ]
]);

The pairs definitions are the same as for creating either a InnerList or an Item using their respective fromPair method.

Adding and updating parameters

To ease working with instances that have a Parameters object attached to, the following methods are added:

use Bakame\Http\StructuredFields\ByteSequence;
use Bakame\Http\StructuredFields\InnerList;
use Bakame\Http\StructuredFields\Item;
use Bakame\Http\StructuredFields\Token;

//@type SfItemInput ByteSequence|Token|DateTimeInterface|string|int|float|bool

Item::fromAssociative(SfItemInput $value, Parameters|iterable<string, SfItemInput> $parameters): self;
Item::fromPair(array{0:SfItemInput, 1:Parameters|iterable<array{0:string, 1:SfItemInput}>} $pair): self;

InnerList::fromAssociative(iterable<SfItemInput> $value, Parameters|iterable<string, SfItemInput> $parameters): self;
InnerList::fromPair(array{0:iterable<SfItemInput>, Parameters|iterable<array{0:string, 1:SfItemInput}>} $pair): self;

The following example illustrate how to use those methods:

use Bakame\Http\StructuredFields\Dictionary;
use Bakame\Http\StructuredFields\Item;

echo Item::fromAssociative(
        Token::fromString('bar'),
        ['baz' => 42]
    )->toHttpValue(), PHP_EOL;

echo Item::fromPair([
        Token::fromString('bar'),
        [['baz', 42]],
    ])->toHttpValue(), PHP_EOL;

//both methods return `bar;baz=42`

Both objects provide additional modifying methods to help deal with parameters. You can attach and update the associated Parameters instance using the following methods.

$field->addParameter(string $key, mixed $value): static;
$field->appendParameter(string $key, mixed $value): static;
$field->prependParameter(string $key, mixed $value): static;
$field->withoutParameters(string ...$keys): static; // this method is deprecated as of version 1.1 use withoutParametersByKeys instead
$field->withoutAnyParameter(): static;
$field->withParameters(Parameters $parameters): static;

It is also possible to use the index of each member to perform additional modifications.

$field->pushParameters(array ...$pairs): static
$field->unshiftParameters(array ...$pairs): static
$field->insertParameters(int $index, array ...$pairs): static
$field->replaceParameter(int $index, array $pair): static
$field->withoutParametersByKeys(string ...$keys): static
$field->withoutParametersByIndices(int ...$indices): static

The $pair parameter is a tuple (ie: an array as list with exactly two members) where:

  • the first array member is the parameter $key
  • the second array member is the parameter $value

Warning

The return value will be the parent class an NOT a Parameters instance

use Bakame\Http\StructuredFields\InnerList;
use Bakame\Http\StructuredFields\Item;

echo InnerList::new('foo', 'bar')
    ->addParameter('expire', Item::fromDateString('+30 minutes'))
    ->addParameter('path', '/')
    ->addParameter('max-age', 2500)
    ->toHttpValue();

echo InnerList::new('foo', 'bar')
    ->pushParameter(
        ['expire', Item::fromDateString('+30 minutes')],
        ['path', '/'],
        ['max-age', 2500],
    )
    ->toHttpValue();

// both flow return the InnerList HTTP value 
// ("foo" "bar");expire=@1681538756;path="/";max-age=2500

Contributing

Contributions are welcome and will be fully credited. Please see CONTRIBUTING and CODE OF CONDUCT for details.

Testing

The library:

To run the tests, run the following command from the project folder.

composer test

Security

If you discover any security related issues, please email nyamsprod@gmail.com instead of using the issue tracker.

Credits

Attribution

The package internal parser is heavily inspired by previous work done by Gapple on Structured Field Values for PHP.

License

The MIT License (MIT). Please see License File for more information.