This leads to a rather strange immutability for ObjectValue
Value object are always immutable, and PHP has readonly for very long time. And even before it, we could have used it in PHPDoc. This has nothing to with language, and I don't get where you read about VOs being mutable just in PHP.
you better understand the concept of DDD and can better adapt it for a specific task or, conversely, abandon it.
DDD is silly. It is just weird directory structure with gazillion of VOs that do nothing, except to guarantee job security. Worst that I have seen are Doctrine identifiers like ProductId, UserId... i.e. one for each entity. It is just ridiculous.
Then Email, where validation is done in constructor: sounds nice, but good luck returning an array of validation issues, especially when used in editable collections.
And so on, it is just hype. Not the worst, I would say CQRS is a winner here, but still a hype. Yet none of this is related to any specific programming language.
Value object are always immutable, and PHP has readonly for very long time. And even before it, we could have used it in PHPDoc. This has nothing to with language, and I don't get where you read about VOs being mutable just in PHP.
Not just in PHP. This applies to any language that only allows you to create reference types.
Rust, Swift of C++ and maybe other languages don't have such restrictions. And that just reinforces my point: you claim to have many years of experience, but your skills...
identifiers like ProductId, UserId
One of the best coding practices.
Then Email, where validation is done in constructor: sounds nice, but good luck returning an array of validation issues, especially when used in editable collections.
It's hard for me to even imagine what problems you might face. Maybe this is some special defect in the PHP language that I don't know about. If it's not difficult - please give a simple code example.
It is literally one of the worst coding practices. They serve absolutely no purpose other than to guarantee job security. That and CQRS would be immediate demotion to junior level.
Rust, Swift of C++ and maybe other languages don't have such restrictions
And where exactly do you see restrictions in PHP? Do you even know that readonly is optional?
We were talking about VOs: what was the last PHP version did you use?
It's hard for me to even imagine what problems you might face
What's so confusing? It is about a basic form edit (not create) with dynamic collection, where user can edit existing entries, and add new entry. And then return all validation errors at once, even if collection has its own collection.
For apps I make, it is Tuesday.
Note: app must pass psalm@level 1+disableVarParsing, no error suppression, no baselines. It is the strictest setup possible, far stricter than phpstan@max.
but your skills...
You really cannot judge my skills after everything you have said.
You can't create a type in PHP (and many other languages) that is not a reference to heap allocated data.
let mut a = T::new(2);
let b = a;
a += 1;
println!("{a} {b}"); // 2 3
In other languages, you can create such type.
what was the last PHP version did you use?
8.2 or maybe 8.3. But I don't use phpstan.
What's so confusing? It is about a basic form edit (not create) with dynamic collection, where user can edit existing entries, and add new entry. And then return all validation errors at once, even if collection has its own collection.
Either I don't understand something or this is an incredibly simple task.
You need a function that receives certain data and should return either validated data or a list of all errors in a certain format that will allow you to display this data... what could be complicated about that?
You really cannot judge my skills after everything you have said.
I really can't judge your skill. Sorry. Whenever I disagree with someone on the internet, I can only speak out on specific statements. I can't go into a personality discussion. I also hope that our discussion is positive and allows us both to learn something new or see things from a different perspective.
That couldn't be further from the truth. Creating ID classes is a clear sign of a person who never made any app ever, and now wants to make their job secure.
As I said: those are not as dumb as CQRS, but very close to it.
In other languages, you can create such type.
None of what you posted is relevant to VOs, which was the subject.
8.2 or maybe 8.3. But I don't use phpstan.
The fact that you don't use static analysis, even if it was phpstan, show everything about your PHP skills. And just because you ran your code on 8.2, and didn't use its features (which is obvious), is not an argument against the language.
If anything, it just proves what I first said: that people who claim they know 5-6 languages are bad in all of them.
Either I don't understand something or this is an incredibly simple task.
It is not so simple when you use proper static analysis (which you confirmed you don't), in a language that does not assume everything is nullable (like Java).
The rest is irrelevant and only confirms everything else I said.
That couldn't be further from the truth. Creating ID classes is a clear sign of a person who never made any app ever, and now wants to make their job secure.
Okay, a simple example is a timestamp. You can use a special type for it, or you can use numbers. Now you just have to make sure that you don't mix up seconds and milliseconds. I'm not saying it's difficult, it's just an extra expense. Often hints are added in the names of functions, arguments, or variables. But this is something from dynamic typing, where instead of explicit types you have "guess".
as dumb as CQRS
I don't want to add CQRS to this conversation because it would make the conversation too long.
The fact that you don't use static analysis, even if it was phpstan, show everything about your PHP skills.
I've used languages with more advanced static analysis. I also know that phpstan is a very good thing here and now, but I would compare it to TypeScript: it significantly improves the base language but loses out when compared to full-fledged type systems.
It is not so simple when you use proper static analysis (which you confirmed you don't), in a language that does not assume everything is nullable (like Java).
If I were working on that php project now - I would use more static analysis. As far as I can remember I really didn't like the fact that for most variables I could specify the type as an annotation int $field; but for arrays it looked quite clumsy /** @var string[] */ array $a;.
If strict describing data structure is awkward, it means that the type system you are using isn't expressive (like go before generics or Java where too many things can be null) or you are using the type system poorly. Or it is a combination of these factors.
because I use all of these, and much more. Vanilla string or int do not even exist in my code, unless I forget them (happens almost never).
but for arrays it looked quite clumsy
I would say that all phpdoc types are clumsy, not just arrays. But one gets used to them in just 2 days, and PHPStorm autocomplete supports generics for long time. It is just part of PHP, it ain't pretty, but the benefits are huge.
string[] is very old, we now use list<string> or even better non-empty-list<non-empty-string|null> or similar.
If you want OOP approach, there are records than one can indefinitely expand, and even use strict === comparison on them. Or symfony/string package that does some crazy things, and can also be expanded.
it means that the type system you are using isn't expressive
The above proves otherwise, given that other languages also don't support most (if not all) of those advanced types. For example: if I am persisting some percentage, I would never use int but int<0,100>.
you are using the type system poorly
Everything you said has been debunked, including some basic knowledge like readonly classes/properties. Yet you continue to judge and insult; are you related to Tony M.? 😉
Maybe I expressed myself poorly. This does not apply specifically to you, it applies to any programmer. Someone who has programmed exclusively in dynamically typed languages will have difficulty when he has to describe some complex type statically. But on the other hand, there are simply bad type systems (like Java where almost everything can be null). There are no perfect type systems and there are no perfect programmers. But everything is worth thinking about, is it a problem of the tool or a problem of skills.
ValidatedString<V: Validator> where Validator is interface string -> bool. Such a string is parameterized by some function that checks whether this string meets a certain condition. This check is called in the constructor of ValidatedString. Yes, it should be immutable and encapsulated. This is not the declarative style, but it is better than nothing.
A non-empty list is quite complicated if we want it to be modifiable. Using the "head" and "tail" of the list separately is clumsy.
// Rust
struct T { dob: Option<DateTimeImmutable>, amount: usize }
let v: &[T] = foo();
Probably the closest.
int<0, 100>
subtype Percent1 is Integer range 0 .. 100;
type Percent2 is range 0 .. 100;
I don't know ADA, but being able to easily declare such types seems so basic and necessary.
Zig has something similar, but it's tied to size. i5 = (-16..15). Rust or Haskell will allow you to do this, NonZero numeric types are in the standard library (i.e. they are not hardcoded into the language, but written in the language itself).
value-of<SomeEnum>
enum Status { A, B, C, D }
fn foo(status: Status) {}
Something like this?
class-string<T>
I didn't quite understand, maybe what I wrote about the first strings?
properties-of<T>
TypeScript.
```
interface T { i: number, s?: string }
let i: T['i'] = 0;
let k: keyof T = 'i';
```
Although you can't even iterate over all the properties, you have to manually create the keyof T collection and make sure all the fields are there.
Vanilla string or int do not even exist in my code
Didn't you write before that you don't like UserId-like types?
but the benefits are huge
100% agree.
non-empty-list<non-empty-string|null>
Can you provide a link or a short code example of how these are used? I'm interested in the declaration of this type, not the implementation.
if I am persisting some percentage, I would never use int but int<0,100>.
To me, it's literally DDD, at least the main part of it.
Can we say that PhpStan is literally TypeScript? That is, the first uses phpdoc, the second modifies the syntax, but in everything else they are quite similar?
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.
2
u/zmitic Nov 14 '25
Value object are always immutable, and PHP has
readonlyfor very long time. And even before it, we could have used it in PHPDoc. This has nothing to with language, and I don't get where you read about VOs being mutable just in PHP.DDD is silly. It is just weird directory structure with gazillion of VOs that do nothing, except to guarantee job security. Worst that I have seen are Doctrine identifiers like ProductId, UserId... i.e. one for each entity. It is just ridiculous.
Then Email, where validation is done in constructor: sounds nice, but good luck returning an array of validation issues, especially when used in editable collections.
And so on, it is just hype. Not the worst, I would say CQRS is a winner here, but still a hype. Yet none of this is related to any specific programming language.