rasuvaeff/rector-datetime-immutable
Composer 安装命令:
composer require rasuvaeff/rector-datetime-immutable
包简介
Rector rules that migrate DateTime to DateTimeImmutable and auto-fix lost mutations whose return value is ignored
README 文档
README
Rector rules that migrate mutable DateTime to
DateTimeImmutable — and auto-fix the lost mutations the migration
creates, the classic silent bug where $date->modify('+1 day'); throws the
new instance away:
// before — mutable construction, in-place mutation $deadline = new \DateTime('2026-01-01'); $deadline->modify('+1 month'); // after (both rules) — immutable, and the mutation result is kept $deadline = new \DateTimeImmutable('2026-01-01'); $deadline = $deadline->modify('+1 month');
PHPStan (level 4) and Psalm report ignored DateTimeImmutable mutator
results; this package is the piece that fixes them in bulk during a
migration.
Using an AI coding assistant? llms.txt has a compact reference you can pass as context.
TL;DR
Two ways to run the migration:
| Path | How |
|---|---|
| CLI wrapper (recommended) | vendor/bin/rector-datetime-immutable src — boundary preflight, migration to convergence and a diagnostic pass in one command; see One-command migration |
Manual rector.php |
register the rules yourself; see Manual Rector setup |
Manual setup warning: one Rector run cannot both migrate and repair — run
vendor/bin/rector process until it reports no changes (usually twice),
otherwise the lost mutations created by the first pass stay in the code. The
wrapper does this for you.
Table of contents
Requirements
- PHP 8.3 - 8.5 to run the rules
rector/rector^2.5webmozart/assert^1.11 || ^2.0proc_openenabled when using the convergence wrapper — available in a default PHP build unless the host disables it viadisable_functions
Installation
composer require --dev rasuvaeff/rector-datetime-immutable
Usage
One-command migration
The installed Composer binary first runs a read-only mutable-boundary preflight,
applies the default migration repeatedly until a clean confirmation pass, then
runs LostDateTimeMutationRector in MODE_REPORT without changing the files:
vendor/bin/rector-datetime-immutable src
The command edits the selected paths. Commit or stash unrelated work first. Typical output:
Preflight: no mutable DateTime boundaries found.
Migration pass 1: 12 changed file(s).
Migration pass 2: 4 changed file(s).
Migration pass 3: 0 changed file(s).
Converged after 2 change-producing pass(es).
Diagnostic pass: no manual review cases found.
Summary: 14 file(s) changed across 2 change-producing pass(es); 0 manual review case(s).
If preflight finds a native, inherited, abstract/interface or vendor callable
whose parameter accepts DateTime but rejects DateTimeImmutable, or a method
parameter that feeds a property the migration preserves as mutable, it prints
file:line entries plus a resolution hint per finding category, exits with
code 2 and changes no files. The same exit is used after convergence when the
lost-mutation report finds a case that cannot be assigned safely.
| Exit | Meaning |
|---|---|
0 |
migration converged and no manual cases remain |
1 |
Rector/process/JSON failure |
2 |
preflight blocked migration or post-migration manual review remains |
3 |
migration did not converge within the pass limit |
64 |
invalid wrapper arguments |
Useful options:
vendor/bin/rector-datetime-immutable --dry-run src # full preview, no writes vendor/bin/rector-datetime-immutable --acknowledge-boundaries src vendor/bin/rector-datetime-immutable --doctrine-columns src # co-migrate ORM columns vendor/bin/rector-datetime-immutable --format=github src # or --format=json vendor/bin/rector-datetime-immutable --max-passes=8 src tests vendor/bin/rector-datetime-immutable --no-report src vendor/bin/rector-datetime-immutable \ --preflight-config=rector-preflight.php \ --config=rector-migration.php \ --report-config=rector-report.php \ src
The packaged defaults are config/preflight.php, config/migration.php and
config/report.php. Use custom configs for project-specific skips, staged
options or ALLOW_SUBCLASS.
Dry-run preview
--dry-run copies the paths into a temporary workspace, runs the whole flow
there — preflight, convergence, diagnostic pass — prints every would-be diff
with paths mapped back to the originals and changes no project file. Exit
codes keep their meaning, so the preview also tells you how the real run
would end. Declarations outside the copied paths (vendor classes, parents in
directories you did not pass) are still read from their original files; the
write run remains authoritative.
CI output
--format=github keeps the human output and additionally emits
::error file=…,line=…::… workflow annotations for preflight blockers and
::warning … for manual review cases, so the migration PR shows every finding
inline.
--format=json suppresses narration and prints a single machine-readable
object on stdout: status (clean, blocked, manual-review,
not-converged, acknowledged), exitCode, per-pass passes,
changedFiles, and the preflight/manualReview/acknowledged findings as
{file, line, message, category} where category is one of
requires-datetime, feeds-mutable-property, lost-mutation, diagnostic.
With --dry-run the object also carries the would-be diffs.
Resolving preflight findings
| Finding | Resolution |
|---|---|
parameter $x feeds mutable property $y |
mark the enclosing method @mutable-datetime — its signature and connected call-site arguments stay mutable — co-migrate ORM columns with --doctrine-columns, or migrate the storage contract first |
parameter $x requires DateTime |
rewrite the call to a DateTimeImmutable-safe API, or review the flow and acknowledge it |
@mutable-datetime on the calling method does not silence a
requires DateTime finding: the marker preserves that method's own contract,
while the finding points at the called native/vendor/inherited parameter. The
migration itself keeps values connected to such a callable by simple
assignments mutable, so once the flow is reviewed, acknowledge it:
vendor/bin/rector-datetime-immutable --acknowledge-boundaries src
This writes a self-documenting comment above every boundary call and re-runs the preflight:
// @mutable-datetime-boundary: parameter $object requires DateTime date_modify($moment, '+1 hour');
A statement carrying @mutable-datetime-boundary is skipped by all further
preflights — the review lives in the code and survives reruns. Findings of
the feeds mutable property kind are never auto-acknowledged: silencing
them would let the migration break the property assignment at runtime, so
they keep their own resolutions above.
File-level skipping through a custom preflight config remains available as the coarse alternative:
// rector-preflight.php <?php declare(strict_types=1); use Rasuvaeff\RectorDateTimeImmutable\MutableDateTimeBoundaryRector; use Rector\Config\RectorConfig; return RectorConfig::configure() ->withRules([ MutableDateTimeBoundaryRector::class, ]) ->withSkip([ MutableDateTimeBoundaryRector::class => [ __DIR__ . '/src/Legacy/SdkAdapter.php', ], ]);
vendor/bin/rector-datetime-immutable --preflight-config=rector-preflight.php src
After convergence the same exit 2 reports lost mutations the fix mode cannot
assign safely — resolve those by assigning the mutator result yourself
($date = $date->modify(...)).
Doctrine columns co-migration
By default ORM-mapped members are preserved. --doctrine-columns (the
DOCTRINE_COLUMNS option of both rules) opts into co-migrating
attribute-mapped columns: the property, its accessors and connected
constructor parameters migrate together with the mapping, which moves to the
native immutable DBAL variant — same database schema, immutable hydration.
#[ORM\Column(type: 'datetime')] // → type: 'datetime_immutable' private \DateTime $expiresAt; // → private \DateTimeImmutable $expiresAt; #[ORM\Column(type: Types::DATETIME_MUTABLE)] // → Types::DATETIME_IMMUTABLE #[ORM\Column] // no type: Doctrine infers it from the PHP type
Covered mappings: datetime, date, time, datetimetz as string literals
or the matching Types::*_MUTABLE constants, plus columns without a type
argument. Custom type strings, dynamic type expressions, positional attribute
arguments and docblock @ORM\Column annotations stay preserved. Requires
doctrine/dbal ≥ 2.6 (native *_immutable types). Review lifecycle code
that mutated entity dates in place — the diagnostic pass reports it as lost
mutations.
Manual Rector setup
// rector.php <?php declare(strict_types=1); use Rasuvaeff\RectorDateTimeImmutable\DateTimeImmutableRector; use Rasuvaeff\RectorDateTimeImmutable\LostDateTimeMutationRector; use Rector\Config\RectorConfig; return RectorConfig::configure() ->withPaths([__DIR__ . '/src']) ->withRules([ DateTimeImmutableRector::class, LostDateTimeMutationRector::class, ]);
Without the wrapper, run vendor/bin/rector process until it reports no
changes (usually twice): within one run type inference still sees the
pre-migration types, so lost mutations created by the construction migration
become visible to the next run.
Run MutableDateTimeBoundaryRector separately with --dry-run before a manual
migration. Do not combine it with migration rules: its comments are diagnostic
diff markers, not source changes to commit.
MutableDateTimeBoundaryRector
Reports arguments flowing into stable callables whose declared parameter
accepts DateTime but rejects DateTimeImmutable. Stable callables are native
PHP functions, vendor functions/methods, interface or abstract methods, and
methods constrained by an ancestor or marked @mutable-datetime. Other local
concrete callables are not reported because their declarations migrate together
with their call sites.
The analysis supports positional, named and variadic arguments across functions, instance/static methods and constructors. The CLI runs this rule as a mandatory dry-run before changing files.
The rule also reports method parameters that feed a property the migration
preserves as mutable — ORM columns, @mutable-datetime declarations,
inherited properties ($this->ormColumn = $param;, including ??/ternary
branches): migrating such a parameter guarantees a TypeError on the property
assignment. Resolve by marking the method @mutable-datetime (its signature
and connected call-site arguments then stay mutable), by co-migrating ORM
columns with --doctrine-columns, or by migrating the storage contract
itself.
Statements carrying the @mutable-datetime-boundary comment are skipped as
already-reviewed boundary calls. The rule's MODE option selects report
(default — attach @todo diff markers) or acknowledge (write the
@mutable-datetime-boundary comment above every boundary call; used by the
CLI's --acknowledge-boundaries). Feed findings are never written in
acknowledge mode.
DateTimeImmutableRector
Migrates DateTime construction and concrete type declarations to
DateTimeImmutable.
| Option | Default | What it enables |
|---|---|---|
CONSTRUCTORS |
true |
new \DateTime(...), shared static factories including createFromTimestamp(), and the two procedural date_create*() factories |
TYPEHINTS |
true |
\DateTime in named functions, methods, closures, arrow functions and enum methods (incl. nullable and union types) |
PROPERTIES |
true |
\DateTime in typed properties and promoted constructor parameters |
ALLOW_SUBCLASS |
false |
rewrite class X extends \DateTime to extends \DateTimeImmutable (risky — downstream in-place mutation breaks; pair with LostDateTimeMutationRector) |
DOCTRINE_COLUMNS |
false |
co-migrate attribute-mapped Doctrine columns together with their mapping type (see «Doctrine columns co-migration») |
The migration also keeps the file consistent:
- the
@var/@param/@returndocblock types of a migrated declaration (including the@psalm-/@phpstan-tag variants) are rewritten toDateTimeImmutable— only the type token changes, descriptions stay; docblock-only declarations without a native type are never rewritten; - a
use DateTime;import (aliased or not) is removed once nothing in the file references it anymore — code, docblock and comment references all count, and the scanner errs toward keeping the import.
->withConfiguredRule(DateTimeImmutableRector::class, [ DateTimeImmutableRector::CONSTRUCTORS => true, DateTimeImmutableRector::TYPEHINTS => true, DateTimeImmutableRector::PROPERTIES => true, DateTimeImmutableRector::ALLOW_SUBCLASS => false, ])
Explicitly disabling one category is supported for staged migrations, but an intermediate stage may not be executable until related construction and type declarations are migrated. Run static analysis and tests after every stage.
Never touched:
| Case | Why |
|---|---|
class X extends \DateTime (without ALLOW_SUBCLASS) |
rewriting the parent breaks the subclass's in-place mutation |
| Signatures and properties declared by an ancestor/interface/trait | implementations must preserve inherited contracts |
| Interfaces, traits, abstract classes | their signatures are contracts for implementations |
#[Column] / @ORM\Column mapped members |
the ORM decides the concrete class per mapped type |
Anything whose docblock carries @mutable-datetime |
explicit opt-out marker |
\DateTime::createFromImmutable(...) |
has no DateTimeImmutable counterpart; its containing return type also stays mutable |
| Construction inside anonymous/abstract/trait scopes, plus defaults, direct property assignments and returns feeding preserved mutable contracts | prevents an immutable value from being injected into a skipped declaration without blocking unrelated migrations in the same class/method |
Values connected by simple assignments to a stable DateTime-only callable such as date_modify() or a vendor API |
related parameters, properties, returns and construction stay mutable |
Unions already containing \DateTimeImmutable, including inside a DNF intersection |
rewriting could create a duplicate or redundant type |
Return types whose return directly yields a preserved mutable property, incl. ??/ternary branches |
the runtime value stays DateTime; the migrated declaration would be a guaranteed TypeError |
| Docblock types on declarations without a migrated native type | a docblock-only contract carries no runtime evidence; tags on migrated declarations are synced automatically |
new $class(), intersection types |
not statically provable |
LostDateTimeMutationRector
Finds statement-level mutator calls on a DateTimeImmutable whose return
value is thrown away: modify, add, sub, setDate, setTime,
setISODate, setTimezone, setTimestamp, setMicrosecond.
| Mode | Behaviour |
|---|---|
MODE_FIX (default) |
rewrites $d->modify(...); to $d = $d->modify(...); for directly initialized exact built-in variables and final subclasses/unions; never assigns to $this |
MODE_REPORT |
attaches a // @todo lost DateTimeImmutable mutation… marker comment instead; run with --dry-run to fail CI while leaving code untouched |
->withConfiguredRule(LostDateTimeMutationRector::class, [ LostDateTimeMutationRector::MODE => LostDateTimeMutationRector::MODE_REPORT, ])
Skipped in both modes: used results, mutable receivers, non-subtypes (including
PHPStan @mixin wrappers), and statically visible mutator overrides. Fix mode
also skips $this, property/call receivers, open declared types such as a
DateTimeImmutable parameter, and locals populated by an open return type: a
runtime subclass can override the mutator and legally mutate in place. A local
becomes exact only after an unconditional top-level assignment from direct
built-in construction, a shared static factory, a procedural
date_create_immutable*() factory, a clone of an exact value, or another
proven exact expression. A plain alias ($b = $a;) deliberately does not
establish exactness: in the pre-migration mutable program both names shared
one mutated object, so a receiver-only assignment could silently diverge from
legacy behaviour — such statements stay reported.
Assignments nested in conditionals, loops, switch/try/match branches and
short-circuit expressions never establish exactness and invalidate an open
proof conservatively. Therefore an assignment and lost mutation contained in
the same conditional branch may intentionally remain unchanged. Final
subclasses and unions of final subclasses are safe to fix. Report mode may flag
an open subtype diagnostically because it does not modify the program. Nullsafe
calls ($d?->modify(...)) are out of scope.
MODE_REPORT overlaps with PHPStan level 4 ("call on a separate line has no
effect") — use it only if your pipeline runs Rector without a static analyzer.
Markers
Add @mutable-datetime to a docblock to keep a declaration mutable on
purpose:
/** * @mutable-datetime — third-party SDK mutates this in place */ private \DateTime $sdkClock;
Add @mutable-datetime-boundary as a comment on a call statement to mark a
reviewed boundary call — the preflight then skips it. --acknowledge-boundaries
writes these comments for you:
// @mutable-datetime-boundary: parameter $object requires DateTime date_modify($moment, '+1 hour');
Security
This is a contract-changing migration. The defaults migrate construction and
concrete local declarations together. Typed native/vendor/inherited callable
boundaries, inherited properties/signatures, ORM mappings and dynamic names are
guarded. Dynamic calls, magic dispatch, reflection and untyped external data
flows cannot be proven by a source-to-source rule. Review the diff and run the
full project build after every pass, especially when using staged options or
ALLOW_SUBCLASS, which intentionally changes runtime behaviour of DateTime
subclasses.
Examples
Runnable scripts live in examples/.
Development
make install # composer install (Docker, no local PHP needed) make build # validate + normalize + require-checker + cs + psalm + tests make test # testo (unit + e2e fixtures) make mutation # infection, minMsi=100 — gates the Internal/ decision core; # the rule shells run inside rector subprocesses and are # covered by the e2e fixture suites instead
Mutation testing is scoped to src/Internal/ by design: the decision core
(mutator catalog, factory map, type/docblock rewriters, Doctrine column
detector, marker matcher) runs in-process and is gated at minMsi = 100. The
public rule classes and the CLI execute inside Rector subprocesses, which
Infection cannot observe — they are covered by the e2e fixture suites instead
(byte-compared output, php -l on every transformed file, executed runtime
fixtures). The Infection numbers therefore certify the Internal/ core, not
a package-wide mutation score; see AGENTS.md for the rationale.
License
rasuvaeff/rector-datetime-immutable 适用场景与选型建议
rasuvaeff/rector-datetime-immutable 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 0 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 07 月 15 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「datetime」 「refactoring」 「immutability」 「rector」 「rector-rule」 「datetime-immutable」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 rasuvaeff/rector-datetime-immutable 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 rasuvaeff/rector-datetime-immutable 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 rasuvaeff/rector-datetime-immutable 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Tools to convert between .NET and PHP date formats
PHPSlang is a library that allow you to write a purely functional code in PHP
Time provider, providing consistent current date, time, datetime and timezone across the request.
Editable JSON AST with visitor traversal and formatting-preserving printing.
An extensible package of custom Rector rules for automated PHP refactoring and coding standard enforcement.
Adds the EDTF data type to Wikibase
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 0
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: BSD-3-Clause
- 更新时间: 2026-07-15