perf: reduce per-row and per-access overhead in entity and model casting
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 35/100
- Tipo de issue
- Refactorización
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- php
- Área
- backend, performance
Línea de trabajo
Start with DataCaster::castAs() for the cast-type parsing, DataConverter::reconstruct() for the duplicate syncOriginal() call after injectRawData(), and Entity::__get(), __set() and toArray() for the getter/setter lookup. Run the benchmark or the DataCaster, DataConverter and Entity tests before and after each of the three commits. Done means the listed tests pass, including the documented behavior change for entities that override injectRawData(), and the proposal's timing targets hold.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Problem
Loading rows through a model with casts, especially as entities, repeats the same work on every row and every field:
DataCaster::castAs()parses the type string on every cast. For each value it runsstr_starts_with, amatch, apreg_match(),explode/array_map(trim)and atrim, all to turn something like'?int'or'json-array'into a handler key and params. The handler itself often costs less than the parsing.DataConverter::reconstruct()callssyncOriginal()twice for each entity. It callsinjectRawData(), which already callssyncOriginal(), and then callssyncOriginal()again. When attributes hold arrays or objects, each sync normalizes them and runsjson_encode().Entity::__get()and__set()rebuild the getter or setter name on every access. They runstr_replace/ucwords, then make twomethod_exists()calls.toArray()repeats this for every key.
Measurements
PHP 8.3 on macOS, OPcache on, production mode, warm. The test entity has 10 columns, 6 casts (int, ?int, bool, json-array, float, int-bool) and the default $dates.
| Case | Time |
|---|---|
1000 rows → arrays (fromDataSource) |
3.2 ms |
1000 rows → entities (reconstruct) |
6.3 ms |
| 1000 rows → entity with no casts | 2.0 ms |
| Step | Cost |
|---|---|
castAs() for an int field |
0.33 µs |
IntegerCast::get() called directly |
0.07 µs |
syncOriginal() with an array attribute |
1.2 µs per call |
Getter-name build + 2× method_exists() |
~0.1–0.2 µs per access |
__get() on a plain field |
0.46–0.58 µs |
toArray() with 10 keys |
7.6–11 µs |
Proposal
1. Cache parsed cast types (DataCaster)
Add a private static cache keyed by the type string. It stores only the parsed result: base type, params and the nullable flag.
- Handlers are not cached. Each instance still looks up the handler in its own
$castHandlers, so instances with different custom handlers keep working. - The non-strict
'nullable'param is still added after the cache lookup. - An unknown type still throws on every call.
- Why static: each entity has its own
DataCaster, so a per-instance cache would rarely get a hit. The parse depends only on the string, so the cache is safe across Worker Mode requests. It also can't grow without limit, because type strings come from code, not from input.
2. Sync originals once (DataConverter::reconstruct())
Remove the second syncOriginal() call. injectRawData() already syncs.
3. Per-class getter/setter map (Entity)
On the first access, build a map of each entity class's get*, _get*, set* and _set* methods using ReflectionClass::getMethods(), with lowercase keys. Lookups then replace the two method_exists() calls.
- Bounded memory: the map depends on the class, not on input. Keys passed to
fill()from request data can't make it grow. - Same lookup rules: lowercase keys match PHP's case-insensitive method names. Reflection includes private methods, so results match
method_exists()exactly.
Estimated gains
Fixes 1 and 2 bring 1000 entities from about 6.3 ms to about 3.6 ms, roughly 40% faster. Array results drop from about 3.2 ms to about 1.7 ms. Fix 3 removes about 25–35% of the cost of each __get()/__set() call, which matters most in toArray() and in views that read many properties.
These are estimates from micro-benchmarks, not from the final code.
Behavior changes (4.8)
Only fix 2 changes behavior. If an app entity overrides injectRawData() without calling syncOriginal(), reconstruct() no longer syncs for it. Such an entity will report every field as changed after a find(), and save() will write all columns. That override is already broken for any other caller of injectRawData(). This will be listed in the v4.8.0 changelog under "Behavior Changes", with a note in upgrade_480.rst saying overrides must call syncOriginal().
Fixes 1 and 3 change no behavior.
Not changing
- Lazy
DataCastercreation in theEntityconstructor (~5%). Subclasses can read the protected$dataCasterproperty directly and would getnull. - The non-strict
is_subclass_of()checks and thetoArray()filter closure. Each costs under 2%.
Plan
One PR with three commits, one per fix. Fix 2 also includes the changelog and upgrade-guide entries.
Tests
DataCasterTest:- Two instances with the same type string but different custom handlers each call their own handler.
- Non-strict
?typestill addsnullable, and strict mode doesn't. - An unknown type throws on every call.
DataConverterTest:- A reconstructed entity reports
hasChanged() === false. - An entity that overrides
injectRawData()without syncing now reports changes. This test documents the behavior change.
- A reconstructed entity reports
EntityTest:- Getter lookup ignores case.
_getXwins overgetX.- Inherited getters are found.
- A private getter behaves as it does today.
- Maps are kept separate for each class.
Related
- #10615, #10616, #10617
- Lenguaje dominante
- PHP
- Estrellas
- 6k
- Forks
- 2k
- Merge medio
- 1 d 22 h
- PR fusionados (30 d)
- 59
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de codeigniter4/CodeIgniter4
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
codeigniter4/CodeIgniter4#10616 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 3/5 1-2 días Aptitud para principiantes 62/100
codeigniter4/CodeIgniter4#10617 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 25/100
codeigniter4/CodeIgniter4#10615 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
codeigniter4/CodeIgniter4#10589 · 3 comentarios ·
Los mantenedores suelen responder en 1 día
-
dev
Dificultad 5/5 Más de una semana Aptitud para principiantes 25/100
codeigniter4/CodeIgniter4#10427 · 5 comentarios ·
Los mantenedores suelen responder en 1 día
Todos los issues de codeigniter4/CodeIgniter4
Issues similares
-
No Code Attached Yet
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
joomla/joomla-cms#48611 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
Los mantenedores suelen responder en 1 día
-
Bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Automattic/safe-publish#716 ·
Los mantenedores suelen responder en 1 día
-
[FrameworkBundle][SecurityBundle] Make `KernelBrowser::loginUser()` wor…Posiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. AbiertoFrameworkBundle SecurityBundle
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
symfony/symfony-docs#23277 ·
Los mantenedores suelen responder en 3 días
-
[Sync EN] hard link does not need special permissionsPosiblemente ocupada @lacatoire la tomó hoy. Abiertosync-en
Dificultad 1/5 Menos de una hora Aptitud para principiantes 90/100
Los mantenedores suelen responder en 1 día