|
1 | 1 | # native-sql-mapper |
2 | | -A lightweight extension for the CakePHP ORM that converts prepared PDO statements into fully mapped result sets. It infers entities and associations from CakePHP-style aliases, supports deep and belongsToMany relations, and builds nested entity graphs automatically with strict alias validation. |
| 2 | + |
| 3 | +A lightweight extension for the CakePHP ORM that converts **native SQL queries** (executed through prepared PDO statements) into **fully hydrated CakePHP entity graphs**. |
| 4 | + |
| 5 | +This library allows you to execute raw SQL while still benefiting from CakePHP’s entity system, associations, nested structures, and conventions. It supports **deep associations**, **belongsToMany relations**, **junction data**, **nested mapping**, and **strict alias validation**. |
| 6 | + |
| 7 | +`native-sql-mapper` is ideal when: |
| 8 | + |
| 9 | +- You need SQL performance or features that exceed the ORM’s query builder |
| 10 | +- You want complex joins, window functions, CTEs, subqueries, aggregates |
| 11 | +- You do not want to spend time on converting your SQL statements to query objects using CakePHP's query builder |
| 12 | +- But still want **CakePHP entities**, **patch-like hydration**, and **nested association graphs** automatically built from the result set |
| 13 | + |
| 14 | +Aliases such as: |
| 15 | + |
| 16 | +``` |
| 17 | +Articles__id, |
| 18 | +Articles__title, |
| 19 | +Comments__id, |
| 20 | +Comments__article_id, |
| 21 | +Comments__content |
| 22 | +``` |
| 23 | + |
| 24 | +will be converted into a fully hydrated entity objects. |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +## 🚀 Features |
| 29 | + |
| 30 | +- **Native SQL → real CakePHP entities** |
| 31 | +- **Deep association support** (belongsTo, hasMany, hasOne, belongsToMany) |
| 32 | +- **Automatic nested entity graph building** |
| 33 | +- **Strict alias validation** based on your ORM associations |
| 34 | +- **No configuration required** — conventions are inferred |
| 35 | +- **Works with any SQL** (CTEs, window functions, unions, etc.) |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## 📦 Installation |
| 40 | + |
| 41 | +Install via Composer: |
| 42 | + |
| 43 | +```bash |
| 44 | +composer require bancer/native-sql-mapper |
| 45 | +``` |
| 46 | +--- |
| 47 | + |
| 48 | +## 🔧 Setup & Usage |
| 49 | + |
| 50 | +### 1. Import the trait in your Table class |
| 51 | + |
| 52 | +```php |
| 53 | +use Bancer\NativeQueryMapper\ORM\NativeSQLMapperTrait; |
| 54 | +``` |
| 55 | + |
| 56 | +### 2. Use the trait |
| 57 | + |
| 58 | +```php |
| 59 | +use NativeSQLMapperTrait; |
| 60 | +``` |
| 61 | + |
| 62 | +### 3. Example usage |
| 63 | + |
| 64 | +```php |
| 65 | +$ArticlesTable = $this->fetchTable(ArticlesTable::class); |
| 66 | +$stmt = $ArticlesTable->prepareSQL(" |
| 67 | + SELECT |
| 68 | + id AS Articles__id, |
| 69 | + title AS Articles__title |
| 70 | + FROM articles |
| 71 | + WHERE title = :title |
| 72 | +"); |
| 73 | +$stmt->bindValue('title', 'My Article Title'); |
| 74 | +/** @var \App\Model\Entity\Article[] $entities */ |
| 75 | +$entities = $ArticlesTable->fromNativeQuery($stmt)->all(); |
| 76 | +``` |
| 77 | + |
| 78 | +`$entities` now contains hydrated `Article` entities based on the SQL result. |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## 🔁 hasMany Example Using Minimalistic SQL |
| 83 | + |
| 84 | +```php |
| 85 | +$stmt = $ArticlesTable->prepareSQL(" |
| 86 | + SELECT |
| 87 | + a.id AS Articles__id, |
| 88 | + title AS Articles__title, |
| 89 | + c.id AS Comments__id, |
| 90 | + article_id AS Comments__article_id, |
| 91 | + content AS Comments__content |
| 92 | + FROM articles AS a |
| 93 | + LEFT JOIN comments AS c |
| 94 | + ON a.id=c.article_id |
| 95 | +"); |
| 96 | +$entities = $ArticlesTable->fromNativeQuery($stmt)->all(); |
| 97 | +``` |
| 98 | +`$entities` now contains an array of Article objects with Comment objects as children. |
| 99 | + |
| 100 | +Same as the result of reqular `->find()...->toArray()`: |
| 101 | +```php |
| 102 | +$entities = $ArticlesTable->find() |
| 103 | + ->select(['Articles.id', 'Articles.title']) |
| 104 | + ->contain([ |
| 105 | + 'Comments' => [ |
| 106 | + 'fields' => ['Comments.id', 'Comments.article_id', 'Comments.content'], |
| 107 | + ], |
| 108 | + ]) |
| 109 | + ->toArray(); |
| 110 | +``` |
| 111 | +Notice that `FROM` and `JOIN` clauses may use short or long aliases or no aliases at all (if the query does not use 'hasMany' or 'belongsToMany' associations) but all fields in `SELECT` clause must use aliases according to CakePHP naming convention `{Alias}__{field_name}`. |
| 112 | + |
| 113 | +## 🔁 belongsToMany Example |
| 114 | + |
| 115 | +```php |
| 116 | +$ArticlesTable = $this->fetchTable(ArticlesTable::class); |
| 117 | +$stmt = $ArticlesTable->prepareSQL(" |
| 118 | + SELECT |
| 119 | + Articles.id AS Articles__id, |
| 120 | + Articles.title AS Articles__title, |
| 121 | + Tags.id AS Tags__id, |
| 122 | + Tags.name AS Tags__name |
| 123 | + FROM articles AS Articles |
| 124 | + LEFT JOIN articles_tags AS ArticlesTags |
| 125 | + ON Articles.id=ArticlesTags.article_id |
| 126 | + LEFT JOIN tags AS Tags |
| 127 | + ON Tags.id=ArticlesTags.tag_id |
| 128 | +"); |
| 129 | +$entities = $ArticlesTable->fromNativeQuery($stmt)->all(); |
| 130 | +``` |
| 131 | +You can find more examples in tests - https://github.com/bancer/native-sql-mapper/tree/develop/tests/TestCase/ORM. |
| 132 | + |
| 133 | +### Mapping |
| 134 | +--- |
| 135 | + |
| 136 | +## 🧠 How It Works |
| 137 | + |
| 138 | +- Aliases are parsed using CakePHP’s `Alias__field` naming convention |
| 139 | +- Mapping is validated against real your ORM associations |
| 140 | +- Deep nested associations are built recursively |
| 141 | +- Only entities and associations that exist in your ORM are allowed |
| 142 | + |
| 143 | +--- |
| 144 | + |
| 145 | +## ⚠️ Requirements |
| 146 | + |
| 147 | +- Cake ORM **4.x** or **5.x** (or CakePHP **4.x** or **5.x**) |
| 148 | +- PHP **7.4+** or **8.0+** |
| 149 | +- PDO database driver |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## 📝 Notes & Limitations |
| 154 | + |
| 155 | +- Aliases **must** follow CakePHP-style naming: `Model__field`. |
| 156 | +- If SQL retrieves data from 'hasMany' or 'belongsToMany' associations then all primary columns must be present in `SELECT` clause |
| 157 | +- Fields without valid aliases throw exceptions |
| 158 | +- Associations must exist in the Table class, incorrect aliases throw exceptions |
| 159 | +- Pagination must be handled manually |
| 160 | +- This library is not a replacement of CakePHP query builder but a useful addition to it. |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +## ✔️ Summary |
| 165 | + |
| 166 | +`native-sql-mapper` gives you the **freedom** of native SQL with the **structure** of CakePHP entities. |
| 167 | +It fills the gap between raw PDO statements and the ORM — allowing complex SQL while preserving the integrity of your entity graphs. |
| 168 | + |
| 169 | +--- |
| 170 | +``` |
| 171 | +
|
0 commit comments