Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

perf: reduce per-row and per-access overhead in entity and model casting

Đang mở
#10,618 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
35/100
Loại issue
Tái cấu trúc
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
php
Lĩnh vực
backend, performance

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Problem

Loading rows through a model with casts, especially as entities, repeats the same work on every row and every field:

  1. DataCaster::castAs() parses the type string on every cast. For each value it runs str_starts_with, a match, a preg_match(), explode/array_map(trim) and a trim, all to turn something like '?int' or 'json-array' into a handler key and params. The handler itself often costs less than the parsing.
  2. DataConverter::reconstruct() calls syncOriginal() twice for each entity. It calls injectRawData(), which already calls syncOriginal(), and then calls syncOriginal() again. When attributes hold arrays or objects, each sync normalizes them and runs json_encode().
  3. Entity::__get() and __set() rebuild the getter or setter name on every access. They run str_replace/ucwords, then make two method_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 DataCaster creation in the Entity constructor (~5%). Subclasses can read the protected $dataCaster property directly and would get null.
  • The non-strict is_subclass_of() checks and the toArray() 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 ?type still adds nullable, 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.
  • EntityTest:
    • Getter lookup ignores case.
    • _getX wins over getX.
    • Inherited getters are found.
    • A private getter behaves as it does today.
    • Maps are kept separate for each class.

Related

  • #10615, #10616, #10617
Ngôn ngữ chính
PHP
Star
6k
Fork
2k
Merge trung bình
1 ngày 22 giờ
Pull request đã merge (30 ngày)
59

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của codeigniter4/CodeIgniter4

Tất cả issue của codeigniter4/CodeIgniter4

Issue tương tự

Thêm issue về PHP

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.