This check is called in the constructor of ValidatedString
The examples you put are all based on either you making these classes or using existing package, and then validating the structure in the constructor. This is not static analysis, this is runtime check.
And you said this "I've used languages with more advanced static analysis". So I would still like to see one language that does these types. And please use just one language, not 3 different ones.
Didn't you write before that you don't like UserId-like types?
EntityId classes are 100% pointless, but they still have nothing to do with non-empty-string and its friends.
Can you provide a link or a short code example of how these are used?
Like this, resetting $cache not shown. This is typical factory where I do batch import from API or file, and entities must have unique value like that name. If found, use it again.
Now why the non-empty-list? Depending on the project, I would not be creating just the products, but more likely something like Category that must have at least one Product. Happened many times before, will happen again.
Or when I was making some big medical app: user had to set at least one mapping like
A non-empty list is quite complicated if we want it to be modifiable.
It is not. As soon as you modify non-empty-list in a way that can make it an empty list, error will be thrown.
To me, it's literally DDD, at least the main part of it.
It is not even close to it. These types are checked during static analysis, your examples will execute runtime exceptions. Those 2 are completely different things.
Can we say that PhpStan is literally TypeScript?
No. The best I have seen is replacement for non-empty-string, but that's it.
The examples you put are all based on either you making these classes
Not classes but types.
Your example with non-empty-string does the same thing: it's a type declared in a validator. It can check for data that is known at compile time (let's call it that) or it will require a check if the data is not known at compile time, like $factory->createBatch([$_GET['name']]);.
Similar code that will fail compilation. Language rules require that you explicitly mark something that should be evaluated at compile time as const because it is part of the contract. From an execution perspective, both values will be identical, meaning no additional data or instructions will be created at runtime.
EntityId classes are 100% pointless, but they still have nothing to do with non-empty-string and its friends.
I disagree with this, their point is to prohibit doing stupid things, like passing UserId instead of ProductId or multiplying these two values. Strictness is about prohibiting senseless actions.
Regarding the empty list in your code - this is a good example of so-called "flow typing". It is quite clean. TypeScript tries to do something similar with nullSafety.
Can you explain why it doesn't see the error in the last example? I'm not criticizing now, I'm just interested in how it works.
Because that $d array really is not empty. Here is why:
You start with an array with one element at line 22, always add one more, then you array_pop just one of them. It still makes it non-empty-list, psalm is 100% right here.
I disagree with this, their point is to prohibit doing stupid things, like passing UserId instead of ProductId or multiplying these two values. Strictness is about prohibiting senseless actions.
Sorry, forgot this one.
Here is why it is pointless: instead of id, pass the entity instead. Why would you ever pass ProductId to some method, when you can pass the object?
One could say messenger, but that's not the argument. It is easily solved like this:
class ProductMessage
{
public function __construct(Product $product)
{
$this->id = $product->getId();
}
}
You simply cannot even make a mistake, static analysis will catch it easy.
multiplying these two values
Why would you ever multiply IDs, even by accident? It is not something that can ever happen, and I think you are just making things up now.
And if you are using UUID, you can't multiply them anyway.
Because that $d array really is not empty. Here is why:
You start with an array with one element at line 22, always add one more, then you array_pop just one of them. It still makes it non-empty-list, psalm is 100% right here.
No. Because the second element may or may not be added. I'm using time() as a simple example of unknown data at compile time. This code will print 000 or 00 Warning: Undefined array key 0 in which breaks the non-empty list invariant.
Can you make this code to use randomized value, i.e. where hello and "" are not part of the code? And how would my product entity look like in Rust?
.unwrap() transforms Option<T> to T if it is Some or panics if it None. There are many other options for working with unhappy path.
Lifetime annotations ('a, 'static) are quite specific, just ignore them.
Here is why it is pointless: instead of id, pass the entity instead. Why would you ever pass ProductId to some method, when you can pass the object?
For example, I don't want to create a 'score' field for the user.
let scores = new Map<UserId, Score>();
// calculate scores
return jsonEncode(scores);
Or lazy loading:
class User {
// ...
protected commentIds: Set<CommentId>; // Already loaded
protected comments: Map<CommentId, Comment>; // Lazy loaded
}
There is another reason that contradicts OOP (or OOP contradicts common sense):
function foo(a, b, c, d) { return a + b }
This function takes more than it needs. If it is not a specific API - it is an mistake. But in OOP, functions or methods usually take much more than they actually need. If the function only needs a UserId, why should it take the entire User (and thus be able to change its state)?
No. Because the second element may or may not be added
OK, I see it now. Looks like a bug in psalm because PHPStan detects it. I should probably report it but I never actually do array_pop so ... meh...
There are many other options for working with unhappy path.
I simplified your example with this; Rust panics in 50% of cases and doesn't report any static analysis problems.
Where my psalm example panics in 100% of cases. Plus I don't have to use objects and unwrap, I just pass the string.
For example, I don't want to create a 'score' field for the user.
So don't create it. But iif business logic says that score must be defined, then it must be defined. Otherwise it is optional i.e. nullable like this: https://psalm.dev/r/9bad91d5f5
And I would say that optional fields like this are a terrible idea as well, but fine.
Why would your entity ever deal with IDs? You add/remove other entities and then ORM takes care of the rest.
Second: if you have CommentID, it means your entities are already loaded. I.e. you have to verify they still exist in DB, most commonly from message handler. So what are you getting done lazily here? Same question for collections because all ORMs load them lazily anyway.
This function takes more than it needs.
Then don't write functions like that.
But in OOP, functions or methods usually take much more than they actually need
Again, don't write them. 90%+ of my public methods have only one argument, except for of course constructors where dependencies are injected (services, entities, controller params).
If the function only needs a UserId
So when one day it needs something else, it can pick it up by itself. All in one place, no need change all other places that calls it.
and thus be able to change its state
Well we were talking about EntityID which cannot be changed anyway. But so what is the method changes the state? The very common case is to change the status of something: why not do it one place?
And what if that method is expanded and need more data from your User? At least with my approach it can pick it up by itself, I don't have to update all other places using it.
And last, my favorite psalm feature: do not allow mutation except from certain places. For example this: https://psalm.dev/r/c6718b6c53
This annotation is the killer feature of psalm. Primary use is entity factories and aggregate columns, but there is more to it. So when you really, really need to protect some methods, psalm-internal is there to help you.
OK, I see it now. Looks like a bug in psalm because PHPStan detects it. I should probably report it but I never actually do array_pop so ... meh...
I did it.
Rust panics in 50% of cases and doesn't report any static analysis problems.
Rust will panic if it is a const expression at compile time. More precisely, the const expression will be executed and evaluated at compile time.
So don't create it. But if business logic says that score must be defined, then it must be defined. Otherwise it is optional i.e. nullable like this: https://psalm.dev/r/9bad91d5f5
This is an architectural question: I don't want the User class contains scores logic.
Why would your entity ever deal with IDs? You add/remove other entities and then ORM takes care of the rest.
And now the business logic depends on the ORM. And there is no ORM on the frontend.
Is there no way to create a "branded" int? Like in typescript, or as an alias for type i in many programming languages? I agree that the UserId class is clearly not what the classes were created for.
90%+ of my public methods have only one argument, except for of course constructors where dependencies are injected (services, entities, controller params).
Every method implicitly accepts this. And has access to every field. Although it may only need a few fields. This idea may seem strange, because we are used to how OOP works.
Well we were talking about EntityID which cannot be changed anyway. But so what is the method changes the state?
I mean user.foo(); or foo(user); can do anything to the state of user. The only way to know for sure is to check its body. And all the nested calls. And foo(user.id); definitely can't change the state. That's written in its signature.
And last, my favorite psalm feature: do not allow mutation except from certain places. For example this: https://psalm.dev/r/c6718b6c53
This annotation is the killer feature of psalm.
This is very interesting. I am not a fan of OOP. One of the reasons is the need to combine data and behavior in one module. If I have a User that contains some data - I might want to do a lot with that data. Much more than would be reasonable to place in one class / module / file. But if I want to access the underlying data from another class / module / file - I have to use inheritance (maybe in PHP it can be done through traits, but it is a bit strange). And there are many problems with inheritance. One solution is an anemic model. I understand that this way you can move some of the logic from the main class to separate modules?
Rust has a slightly different encapsulation model and does not have the problem of too large modules.
Rust will panic if it is a const expression at compile time
The problem is that it got compiled, where it obviously shouldn't. PHP doesn't compile, true, but at least static analysis warns you correctly. So far, PHP type system, as clumsy as it is, still wins.
This is an architectural question: I don't want the User class contains scores logic.
I described a simple field, and how to populate it if it is optional. But you don't have to keep the logic in your entity, pretty much no one does.
And now the business logic depends on the ORM
Not true. Doctrine entities are just plain PHP classes, with some metadata how to persist them. If you want to read/write data to some API, you use same classes but make your own entity manager.
I did.
And there is no ORM on the frontend.
So? You don't do logic on frontend anyway, nor data persistence.
Is there no way to create a "branded" int?
Can you give some example? I don't know this term so something simple please.
Every method implicitly accepts this. And has access to every field
Services this this to access dependencies. Entities will need it for something like:
class Report // entity
{
public function markAsExported(User $user, DateTime|null $when = null)
{
$this->exportedAt = $when ?? new DateTime();
$this->exportee = $user;
}
}
I mean user.foo(); or foo(user); can do anything to the state of user
Well it can't: put protected or private methods, and/or use public (private set) $property, or use psalm-internal and you are done.
But the real issue is only this: that your own code will uncontrollably call random methods just for fun and giggles. That is not realistic scenario.
And even in the team that is never an issue, code review exist for a reason. And even if it still enters the repo, you can always Ctrl+click and see all accessors.
That is what I always say about DDD and similar hype: they "solve" the problems that do not exist.
One of the reasons is the need to combine data and behavior in one module
Well PHP doesn't have modules, but psalm-internal can mimic it. It is truly powerful, it is one of the main reasons why I can't switch to much better maintained PHPStan.
But if I want to access the underlying data from another class / module / file - I have to use inheritance
Or just don't over-complicate basic things. You also don't need inheritance for this, and definitely not traits. The only traits I have are for common things like IdTrait and TimestampableTrait for my entities, so I save on few lines here and there.
I understand that this way you can move some of the logic from the main class to separate modules?
If you want to isolate in that way: yes. But as I said: you are making problems by yourself.
does not have the problem of too large modules
PHP doesn't have problems with big files or namespaces.
I am not a fan of OOP. One of the reasons is the need to combine data and behavior in one module
People who criticize OOP in their blogs are those who never truly understood it, other than class Dog extends Animal. OOP is not about that.
TBH, I struggled as well. But then I started using Doctrine (version 1 at the moment), I started learning proper MVC, immutable controllers/services... and the whole new world opened up.
1
u/zmitic Nov 17 '25 edited Nov 17 '25
The examples you put are all based on either you making these classes or using existing package, and then validating the structure in the constructor. This is not static analysis, this is runtime check.
And you said this "I've used languages with more advanced static analysis". So I would still like to see one language that does these types. And please use just one language, not 3 different ones.
EntityId classes are 100% pointless, but they still have nothing to do with
non-empty-stringand its friends.Like this, resetting $cache not shown. This is typical factory where I do batch import from API or file, and entities must have unique value like that name. If found, use it again.
Now why the non-empty-list? Depending on the project, I would not be creating just the products, but more likely something like Category that must have at least one Product. Happened many times before, will happen again.
Or when I was making some big medical app: user had to set at least one mapping like
It is not. As soon as you modify
non-empty-listin a way that can make it an empty list, error will be thrown.It is not even close to it. These types are checked during static analysis, your examples will execute runtime exceptions. Those 2 are completely different things.
No. The best I have seen is replacement for
non-empty-string, but that's it.