PHP

385 readers
1 users here now

<?

namespace lemmy\php;

/*

Welcome to the PHP community on Lemmy

#Rules:

1: Soon(TM)

#Helpful stuff:

PHP Documentation

Composer

PHP Standards

#Common frameworks:

Symfony

Larvel

*/

echo "Welcome";

founded 2 years ago
MODERATORS
1
 
 

cross-posted from: https://chrastecky.dev/post/16

Starting with PHP 8.5, you'll be able to do the following:

 public function __construct(
    final public string $someProperty,
) {}

This wasn't possible before, as promoted properties couldn't be declared final.

Perhaps the more interesting part is that you can now omit the visibility modifier if you include final. In that case, the property will default to public:

 public function __construct(
    final string $someProperty, // this property will be public
) {}

Personally, I’m not a fan of this behavior — I prefer explicit over implicit. Fortunately, it can be enforced by third-party tools like code style fixers. Still, I would have preferred if the core required the visibility to be specified.

What do you think? Do you like this change, or would you have preferred a stricter approach?

2
 
 

cross-posted from: https://chrastecky.dev/post/13

This change is quite straightforward, so this won’t be a long article. PHP 8.5 adds support for annotating non-class, compile-time constants with attributes. Compile-time constants are those defined using the const keyword, not the define() function.

Attributes can now include Attribute::TARGET_CONSTANT among their valid targets. Additionally, as the name suggests, Attribute::TARGET_ALL now includes constants as well. The ReflectionConstant class has been updated with a new method, getAttributes(), to support retrieving these annotations.

One particularly useful aspect of this change is that the built-in #[Deprecated] attribute can now be applied to compile-time constants.

As promised, this was a short post, since the change is relatively simple. See you next time—hopefully with a more exciting new feature in PHP 8.5!

3
 
 

cross-posted from: https://chrastecky.dev/post/15

PHP has long had a levenshtein() function, but it comes with a significant limitation: it doesn’t support UTF-8.

If you’re not familiar with the Levenshtein distance, it’s a way to measure how different two strings are — by counting the minimum number of single-character edits (insertions, deletions, or substitutions) required to change one string into another.

For example, the following code returns 2 instead of the correct result, 1:

var_dump(levenshtein('göthe', 'gothe'));

There are workarounds — such as using a pure PHP implementation or converting strings to a custom single-byte encoding — but they come with downsides, like slower performance or non-standard behavior.

With the new grapheme_levenshtein() function in PHP 8.5, the code above now correctly returns 1.

Grapheme-Based Comparison

What makes this new function especially powerful is that it operates on graphemes, not bytes or code points. For instance, the character é (accented 'e') can be represented in two ways: as a single code point (U+00E9) or as a combination of the letter e (U+0065) and a combining accent (U+0301). In PHP, you can write these as:

$string1 = "\u{00e9}";
$string2 = "\u{0065}\u{0301}";

Even though these strings are technically different at the byte level, they represent the same grapheme. The new grapheme_levenshtein() function correctly recognizes this and returns 0 — meaning no difference.

This is particularly useful when working with complex scripts such as Japanese, Chinese, or Korean, where grapheme clusters play a bigger role than in Latin or Cyrillic alphabets.

Just for fun: what do you think the original levenshtein() function will return for the example above?

var_dump(levenshtein("\u{0065}\u{0301}", "\u{00e9}"));
4
 
 

You may hate me for this, but this is what's really going on. I love JS/TS and Node but the world is built in economies of scale not in love for programming languages... and PHP is the best when it comes to cheap scaling.

Really hope someone at Node decides to make it fast-cgi compatible in a nice way.

5
 
 

cross-posted from: https://lemmy.world/post/28692919

uSentry is a lightweight, self-hosted Identity and Access Management (IAM) and Single Sign-On (SSO) solution designed for homelab and small-scale environments.

⚡ A single PHP file. < 400 lines of code. No database. No background processes. No cloud. Just works. ⚡

Most IAM and SSO solutions require databases, certificates and background services baked into a dozen containers. This is all fine but also also overkill for homelabs and impossible for low-power ARM devices. uSentry is different, it isn't pretty but it sucks less for a lot of use cases.

Enjoy!

6
 
 

cross-posted from: https://chrastecky.dev/post/6

Lazy objects allow you to delay initialization until it’s absolutely necessary. This is particularly useful when an object depends on I/O operations—such as accessing a database or making an external HTTP request. Although you could previously implement lazy loading in userland, there were significant caveats. For example, you couldn’t declare the proxied class as final, because the lazy proxy must extend it to satisfy type checks. If you’ve ever used Doctrine, you might have noticed that entities cannot be declared final for precisely this reason.

Without further ado, let's dive right in!

Lazy deserializer

For this project, I created a simple DTO:

final readonly class Product
{
    public function __construct(
        public string $name,
        public string $description,
        public float $price,
    ) {
    }
}

Notice that the class is declared as both final and readonly—something that wouldn’t have been possible with a pure userland implementation. Here’s what the deserializer looks like:

final readonly class LazyDeserializer
{
    /**
     * @template T of object
     * @param class-string<T> $class
     * @return T
     */
    public function deserialize(array $data, string $class): object
    {
        // todo
    }
}

This setup lets us write code like the following:

$data = [
    'name' => 'Door knob',
    'description' => "The coolest door knob you've ever seen!",
    'price' => 123.45,
];

$deserializer = new LazyDeserializer();
$object = $deserializer->deserialize($data, Product::class);

var_dump($object);

Implementing the deserializer

I split the implementation into multiple methods for better maintainability. Let’s start with the single public method whose signature we just saw:

    /**
     * @template T of object
     * @param class-string<T> $class
     * @return T
     */
    public function deserialize(array $data, string $class): object
    {
        $reflection = new ReflectionClass($class);

        return $reflection->newLazyGhost(function (object $object) use ($data): void {
            $this->deserializeObject($data, $object);
        });
    }

First, we obtain a reflection of the target class and then call its newLazyGhost method. The lazy ghost is responsible for creating the lazily initialized object. It accepts a single callback that receives an instance of the target object (which remains uninitialized) and uses it to set up the properties in the deserializeObject method.

At this point, the method returns an object of the target class (specified by the $class parameter) with all its properties uninitialized. These properties will be initialized only when you access them. For example, if you var_dump the resulting object right now, you might see something like:

lazy ghost object(App\Dto\Product)#7 (0) {
  ["name"]=>
  uninitialized(string)
  ["description"]=>
  uninitialized(string)
  ["price"]=>
  uninitialized(float)
}

Notice that it doesn’t matter that the private deserializeObject method isn’t implemented yet—the object remains truly lazy. Any errors related to initialization will only appear when you try to access one of its uninitialized properties.

Here's an implementation of the private method:

    private function deserializeObject(array $data, object $object): void
    {
        $reflection = new ReflectionObject($object);

        foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
            if (!isset($data[$property->getName()])) {
                if ($property->getType()?->allowsNull()) {
                    $property->setValue($object, null);
                }
                continue;
            }

            $property->setValue($object, $data[$property->getName()]);
            unset($data[$property->getName()]);
        }

        if (count($data)) {
            throw new LogicException('There are left-over data in the array which could not be deserialized into any property.');
        }
    }

I’m using reflection here because the object is marked as readonly—this is the only way to set a readonly property outside the constructor. If the properties weren’t readonly, you could simply assign values directly (e.g. $object->$propertyName = $value).

The process is straightforward: we iterate over each public property of the class, assign the corresponding value from the data array, and if a property is missing (and its type allows null), we set it to null. Finally, we ensure there’s no leftover data, which would indicate a mismatch between the data and the model. (Note that this is a naive implementation; real-world deserializers tend to be more robust.)

Now, let’s modify the previous example slightly to trigger the initialization of the model:

$data = [
    'name' => 'Door knob',
    'description' => "The coolest door knob you've ever seen!",
    'price' => 123.45,
];

$deserializer = new LazyDeserializer();
$object = $deserializer->deserialize($data, Product::class);

var_dump($object); // this will print the uninitialized model

$object->name; // simply calling a property will force the object to initialize

var_dump($object); // this now prints:

// object(App\Dto\Product)#7 (3) {
//  ["name"]=>
//  string(9) "Door knob"
//  ["description"]=>
//  string(39) "The coolest door knob you've ever seen!"
//  ["price"]=>
//  float(123.45)
//}

Note that this implementation isn’t very useful on its own since it merely assigns properties from a static array—there’s no I/O involved. Let’s enhance it to support deserializing more complex values, such as enums, nested objects, and (most importantly) I/O-bound entities (which we’ll simulate with an HTTP request). First, instead of directly assigning the value, I add another private method:

$property->setValue($object, $this->assignValue($property, $data[$property->getName()]));

Now, let’s implement assignValue:

    private function assignValue(ReflectionProperty $property, mixed $value): mixed
    {
        $type = $property->getType();
        if (!$type) {
            return $value;
        }
        if ($value === null && $type->allowsNull()) {
            return null;
        }
        if (!$type instanceof ReflectionNamedType) {
            throw new LogicException('Only a single type is allowed');
        }

        $typeName = $type->getName();
        if (is_a($typeName, BackedEnum::class, true)) {
            return $typeName::from($value);
        } else if (is_array($value) && class_exists($typeName)) {
            return $this->deserialize($value, $typeName);
        } else if ($this->isHttpEntity($typeName) && is_string($value)) {
            return $this->fetchHttpEntity($typeName, $value);
        }

        return $value;
    }

Here’s what happens in assignValue:

  • If the property has no type, the value is returned as is.
  • If the value is null and the type is nullable, null is returned.
  • An exception is thrown if the type isn’t a single named type (supporting multiple types would add too much complexity for this example).
  • Three cases are then handled:
    • If the type is a backed enum, we convert the value using its built-in from method.
    • If the value is an array and the type corresponds to an existing class, we recursively call deserialize to support nested objects.
    • If the type is marked as a HTTP entity (using the HttpEntity attribute) and the value is a string, we assume it represents an ID and fetch the entity.

Here are some more objects that the deserializer now supports:

enum Availability: int
{
    case InStock = 1;
    case OnTheWay = 2;
    case OutOfStock = 3;
}

final readonly class ProductVariant
{
    public function __construct(
        public string $color,
        public string $size,
    ) {
    }
}

#[HttpEntity]
final readonly class Seller
{
    public function __construct(
        public string $id,
        public string $name,
        public float $rating,
    ) {
    }
}

For completeness, here’s the definition of the HttpEntity attribute and a helper method to check for it:

#[Attribute(Attribute::TARGET_CLASS)]
final readonly class HttpEntity
{
}

private function isHttpEntity(string $typeName): bool
{
    if (!class_exists($typeName)) {
        return false;
    }

    $reflection = new ReflectionClass($typeName);
    $attributes = $reflection->getAttributes(HttpEntity::class);

    return count($attributes) > 0;
}

The enum and the non-HTTP entity class work out of the box. For example:

final readonly class Product
{
    public function __construct(
        public string $name,
        public string $description,
        public float $price,
        public Availability $availability,
        public ?ProductVariant $variant = null,
    ) {
    }
}

$data = [
    'name' => 'Door knob',
    'description' => "The coolest door knob you've ever seen!",
    'price' => 123.45,
    'availability' => 2,
    'variant' => [
        'color' => 'golden',
        'size' => '3',
    ],
];

$deserializer = new LazyDeserializer();
$object = $deserializer->deserialize($data, Product::class);

var_dump($object);

// lazy ghost object(App\Dto\Product)#7 (0) {
//  ["name"]=>
//  uninitialized(string)
//  ["description"]=>
//  uninitialized(string)
//  ["price"]=>
//  uninitialized(float)
//  ["availability"]=>
//  uninitialized(App\Enum\Availability)
//  ["variant"]=>
//  uninitialized(?App\Dto\ProductVariant)
//}

$object->name;

var_dump($object);

// object(App\Dto\Product)#7 (5) {
//  ["name"]=>
//  string(9) "Door knob"
//  ["description"]=>
//  string(39) "The coolest door knob you've ever seen!"
//  ["price"]=>
//  float(123.45)
//  ["availability"]=>
//  enum(App\Enum\Availability::OnTheWay)
//  ["variant"]=>
//  lazy ghost object(App\Dto\ProductVariant)#19 (0) {
//    ["color"]=>
//    uninitialized(string)
//    ["size"]=>
//    uninitialized(string)
//  }
//}

$object->variant->color;

// object(App\Dto\Product)#7 (5) {
//  ["name"]=>
//  string(9) "Door knob"
//  ["description"]=>
//  string(39) "The coolest door knob you've ever seen!"
//  ["price"]=>
//  float(123.45)
//  ["availability"]=>
//  enum(App\Enum\Availability::OnTheWay)
//  ["variant"]=>
//  object(App\Dto\ProductVariant)#19 (2) {
//    ["color"]=>
//    string(6) "golden"
//    ["size"]=>
//    string(1) "3"
//  }
//}

Notice that the variant property is also lazily initialized—which is pretty neat. Every nested object is handled lazily.

I/O bound entities

Now, let’s move on to HTTP entities. We’ll create a service that “fetches” them (in this case, we’ll simulate the fetch):

final readonly class HttpEntityFetcher
{
    public function fetchRawByIdAndType(string $id, string $type): ?array
    {
        sleep(1);
        return [
            'id' => $id,
            'name' => 'Cool seller',
            'rating' => 4.9,
        ];
    }
}

Here, I simulate a slow HTTP request that takes one second to complete and returns JSON data (already decoded into an array). Note that for this example the fetch always returns a seller.

Now all that’s missing is the LazyDeserializer::fetchHttpEntity() method:

public function __construct(
    private HttpEntityFetcher $entityFetcher,
) {
}

/**
 * @template T of object
 *
 * @param class-string<T> $typeName
 * @return T|null
 */
private function fetchHttpEntity(string $typeName, string $id): ?object
{
    return new ReflectionClass($typeName)->newLazyGhost(function (object $object) use ($typeName, $id): void {
        $data = $this->entityFetcher->fetchRawByIdAndType($id, $object::class);
        if (!is_array($data)) {
            throw new InvalidArgumentException('An object of type ' . $typeName . ' with id ' . $id . ' could not be fetched.');
        }

        $this->deserializeObject($data, $object);
    });
}

This lazy ghost postpones the HTTP request until one of the object’s properties is actually accessed. Next, let’s add the seller property to our product:

final readonly class Product
{
    public function __construct(
        public string $name,
        public string $description,
        public float $price,
        public Availability $availability,
        public Seller $seller,
        public ?ProductVariant $variant = null,
    ) {
    }
}

And here’s an example that adds some timing measurements to our deserialization:

$data = [
    'name' => 'Door knob',
    'description' => "The coolest door knob you've ever seen!",
    'price' => 123.45,
    'availability' => 2,
    'variant' => [
        'color' => 'golden',
        'size' => '3',
    ],
    'seller' => 'some-seller-id',
];

$deserializer = new LazyDeserializer(new HttpEntityFetcher());
$start = microtime(true);
$object = $deserializer->deserialize($data, Product::class);
$end = microtime(true);

echo "Deserializer took: " . number_format($end - $start, 10) . " seconds", PHP_EOL;

$start = microtime(true);
$object->seller->name;
$end = microtime(true);

echo "Fetching seller id took: " . number_format($end - $start, 10) . " seconds", PHP_EOL;

On my PC, this prints:

Deserializer took: 0.0000250340 seconds
Fetching seller name took: 1.0002360344 seconds

The deserialization is nearly instantaneous—the delay comes when the HTTP request is eventually executed during initialization.

Partially initializing ghost objects

In the example above, there’s one piece of information we already know about the seller even before any HTTP request is made: its ID. Triggering a network call just to obtain the ID is unnecessary. Fortunately, we can initialize that property immediately:

/**
 * @template T of object
 *
 * @param class-string<T> $typeName
 * @return T|null
 */
private function fetchHttpEntity(string $typeName, string $id): ?object
{
    $reflection = new ReflectionClass($typeName);
    $entity = $reflection->newLazyGhost(function (object $object) use ($typeName, $id): void {
        $data = $this->entityFetcher->fetchRawByIdAndType($id, $object::class);
        if (!is_array($data)) {
            throw new InvalidArgumentException('An object of type ' . $typeName . ' with id ' . $id . ' could not be fetched.');
        }

        unset($data['id']);
        $this->deserializeObject($data, $object);
    });
    $reflection->getProperty('id')->setRawValueWithoutLazyInitialization($entity, $id);

    return $entity;
}

The setRawValueWithoutLazyInitialization method (a catchy name, right?) lets you assign a value to a property without forcing the rest of the object to be initialized.

$start = microtime(true);
$object = $deserializer->deserialize($data, Product::class);
$end = microtime(true);

echo "Deserializer took: " . number_format($end - $start, 10) . " seconds", PHP_EOL;
var_dump($object->seller);

$start = microtime(true);
$object->seller->id;
$end = microtime(true);

echo "Fetching seller id took: " . number_format($end - $start, 10) . " seconds", PHP_EOL;
var_dump($object->seller);

$start = microtime(true);
$object->seller->name;
$end = microtime(true);

echo "Fetching seller name took: " . number_format($end - $start, 10) . " seconds", PHP_EOL;
var_dump($object->seller);

This prints timings similar to:

Deserializer took: 0.0000338554 seconds
Fetching seller id took: 0.0000009537 seconds
Fetching seller name took: 1.0001599789 seconds

As you can see, accessing the ID is immediate, while accessing another property (like the name) triggers the full initialization.

lazy ghost object(App\Entity\Seller)#20 (1) {
  ["id"]=>
  string(14) "some-seller-id"
  ["name"]=>
  uninitialized(string)
  ["rating"]=>
  uninitialized(float)
}

lazy ghost object(App\Entity\Seller)#20 (1) {
  ["id"]=>
  string(14) "some-seller-id"
  ["name"]=>
  uninitialized(string)
  ["rating"]=>
  uninitialized(float)
}

object(App\Entity\Seller)#20 (3) {
  ["id"]=>
  string(14) "some-seller-id"
  ["name"]=>
  string(11) "Cool seller"
  ["rating"]=>
  float(4.9)
}

That’s it for the deserializer example! It’s a simplified implementation, but I imagine that Doctrine may eventually replace its userland proxy approach with these core lazy objects once they target PHP 8.4 and later.

Private key generating example

As a bonus, here’s an additional example—a private key generator that I’ve actually used in one of my libraries. (View on GitHub)

public function generate(int $bits = 4096): KeyPair
{
    $reflection = new ReflectionClass(KeyPair::class);
    $keyPair = $reflection->newLazyGhost(function (KeyPair $keyPair) use ($bits) {
        $config = [
            'private_key_type' => OPENSSL_KEYTYPE_RSA,
            'private_key_bits' => $bits,
        ];
        $resource = openssl_pkey_new($config) ?: throw new CryptographyException('Failed generating new private key');

        $privateKeyPem = '';
        openssl_pkey_export($resource, $privateKeyPem);
        assert(is_string($privateKeyPem));

        $details = openssl_pkey_get_details($resource) ?: throw new CryptographyException('Failed decoding the private key');
        $publicKeyPem = $details['key'];
        assert(is_string($publicKeyPem));

        $reflection = new ReflectionObject($keyPair);
        $reflection->getProperty('privateKey')->setValue($privateKeyPem);
        $reflection->getProperty('publicKey')->setValue($publicKeyPem);

        return $keyPair;
    });
    assert($keyPair instanceof KeyPair);

    return $keyPair;
}

This postpones the expensive operation (generating a 4096 bits private key) until it's actually needed.

7
8
ZCE is dead (home.pearsonvue.com)
submitted 2 years ago by lankybiker@lemmy.world to c/php@lemmy.world
 
 

The ZCE exam is dead

There were a lot of haters, but it is/was the only seriously challenging general PHP qualificiation that I'm aware of

If you could pass it without cheating, then it proved - in my opinion - that you had a good general grasp of PHP concepts rather than being a "framwork developer" who can't really handle raw/unfamiliar PHP code

I can't find any alternatives which is a shame as PHP 8 has brought the language on a lot and it would be great to have a ZCE 8 to go with it

8
9
10
 
 

Link to internals discussion: https://externals.io/message/120972

11
 
 

A good article on implementing an event loop in PHP using the built-in fibers.

12
 
 

The article title is a little clickbaity, but basically, Doctrine and Symfony documentation don't mention that you can use constructor almost anywhere in the documentation.

13
 
 

A while ago I made this Symfony bundle that automagically memoizes your service methods using a PSR-6 cache.

Usage is very simple in 3 steps:

  1. have the service you want to memoize implement any interface
  2. mark the service with the #[Memoizable] attribute
  3. mark methods (or whole service) with the #[Memoize] attribute

Done!

14
 
 

Old coder looking to get back into it. At least as a hobby.. have not worked with php since 2000ish. Any suggestions where to start?

15
16
 
 

Now you can go nuts with all the bots you can think of!

Any feedback welcome.

17
 
 

Why is this needed? Don't Traits already cover this functionality?

18
 
 

What are your thoughts on it? It's performance much better than FPM? Have you used hyperf, or another framework with it, and if so, what do you think about them?

19
20
21
4
Welcome (lemmy.world)
submitted 2 years ago by Madpeter@lemmy.world to c/php@lemmy.world
 
 

Not much here yet but lets change that. news & stuff related to php from around the internet.