PHPackages                             rasuvaeff/rector-datetime-immutable - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. rasuvaeff/rector-datetime-immutable

ActiveLibrary[Testing &amp; Quality](/categories/testing)

rasuvaeff/rector-datetime-immutable
===================================

Rector rules that migrate DateTime to DateTimeImmutable and auto-fix lost mutations whose return value is ignored

v1.0.0(1mo ago)01BSD-3-ClausePHPPHP 8.3 - 8.5CI passing

Since Jul 15Pushed 2w agoCompare

[ Source](https://github.com/rasuvaeff/rector-datetime-immutable)[ Packagist](https://packagist.org/packages/rasuvaeff/rector-datetime-immutable)[ Docs](https://github.com/rasuvaeff/rector-datetime-immutable)[ RSS](/packages/rasuvaeff-rector-datetime-immutable/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (1)Dependencies (11)Versions (3)Used By (0)

rasuvaeff/rector-datetime-immutable
===================================

[](#rasuvaeffrector-datetime-immutable)

[![Stable Version](https://camo.githubusercontent.com/29df7283976bf430d146dd7eda9694817fff412130fc5e50b3b1c66a00b58973/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7261737576616566662f726563746f722d6461746574696d652d696d6d757461626c652e737667)](https://packagist.org/packages/rasuvaeff/rector-datetime-immutable)[![Total Downloads](https://camo.githubusercontent.com/851da8dfaef1365a3a4bfde9830d280d16fabfd8ba358be6e990effd3f4d6436/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7261737576616566662f726563746f722d6461746574696d652d696d6d757461626c652e737667)](https://packagist.org/packages/rasuvaeff/rector-datetime-immutable)[![Build](https://camo.githubusercontent.com/a6104899edd93943cc8ed3fa6d6a0720ffdf9af0a0a57adb4b9f148c1a187f4c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7261737576616566662f726563746f722d6461746574696d652d696d6d757461626c652f6275696c642e796d6c3f6272616e63683d6d6173746572)](https://github.com/rasuvaeff/rector-datetime-immutable/actions)[![Static Analysis](https://camo.githubusercontent.com/0db4e33cdb8412d37926a9b6b92fb0410ac22c2f2f453ea4d8e84d0d0fb0fa03/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7261737576616566662f726563746f722d6461746574696d652d696d6d757461626c652f7374617469632d616e616c797369732e796d6c3f6272616e63683d6d6173746572)](https://github.com/rasuvaeff/rector-datetime-immutable/actions)[![Psalm Level](https://camo.githubusercontent.com/1516496318aec6e2baf9f8ed7888070c8c0c279848ee94165206d1fa8796d864/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5073616c6d2d6c6576656c253230312d627269676874677265656e2e737667)](psalm.xml)[![PHP](https://camo.githubusercontent.com/2191dd1c6903dd3e470edf3e8904dad2976592cf8a41dc1ecf7561aa54073f48/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f7261737576616566662f726563746f722d6461746574696d652d696d6d757461626c652f706870)](https://packagist.org/packages/rasuvaeff/rector-datetime-immutable)[![License](https://camo.githubusercontent.com/f2b333b1f92354dc2f7029da135133936e3ec2889161a6ab179358cdca859e6b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f7261737576616566662f726563746f722d6461746574696d652d696d6d757461626c652e737667)](LICENSE.md)

[Русская версия](README.ru.md)

[Rector](https://getrector.com) 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](llms.txt) has a compact reference you can pass as context.

TL;DR
-----

[](#tldr)

Two ways to run the migration:

PathHow**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](#one-command-migration)**Manual `rector.php`**register the rules yourself; see [Manual Rector setup](#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
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Usage](#usage)
    - [One-command migration](#one-command-migration)
    - [Dry-run preview](#dry-run-preview)
    - [CI output](#ci-output)
    - [Resolving preflight findings](#resolving-preflight-findings)
    - [Doctrine columns co-migration](#doctrine-columns-co-migration)
    - [Manual Rector setup](#manual-rector-setup)
    - [`MutableDateTimeBoundaryRector`](#mutabledatetimeboundaryrector)
    - [`DateTimeImmutableRector`](#datetimeimmutablerector)
    - [`LostDateTimeMutationRector`](#lostdatetimemutationrector)
    - [Markers](#markers)
- [Security](#security)
- [Examples](#examples)
- [Development](#development)
- [License](#license)

Requirements
------------

[](#requirements)

- PHP 8.3 - 8.5 to run the rules
- `rector/rector` ^2.5
- `webmozart/assert` ^1.11 || ^2.0
- `proc_open` enabled when using the convergence wrapper — available in a default PHP build unless the host disables it via `disable_functions`

Installation
------------

[](#installation)

```
composer require --dev rasuvaeff/rector-datetime-immutable
```

Usage
-----

[](#usage)

### One-command migration

[](#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.

ExitMeaning`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 argumentsUseful 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-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

[](#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

[](#resolving-preflight-findings)

FindingResolution`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
