How to Build Custom AI Inside Magento (Without an External Service)
In Part 1 we trained a small model that flags AI-generated product reviews. Training a model is the part everyone writes about. This post is about the part almost nobody does: getting a custom model to actually run inside Magento.
The fake-review detector is the worked example, but the question is general — you have a model (a classifier, a ranker, a scorer) and you need it to do its job inside a Magento store, on real data, in the admin or on the storefront. How do you wire it in? There are two honest answers, and most of the work is choosing between them.
The two ways to integrate a model into Magento

| A · External service | B · In-process (PHP) | |
|---|---|---|
| How it runs | Model behind an HTTP API (Python/GPU) | Model file loaded and scored in PHP |
| Per-call cost | Network round-trip + compute | A function call |
| Extra infra | A service to deploy, scale, monitor | None — ships in the module |
| Works offline | No | Yes |
| Best for | Big models — LLMs, deep nets, GPU | Small models — linear, trees, small nets |
Pattern A is the right call when the model genuinely needs a GPU or gigabytes of weights — an LLM, a vision model, a large transformer. You wrap it in a small service, call it over HTTP, and accept the network and ops cost because there is no alternative.
But a huge number of useful store models are not that. Fraud scores, review filters, lead scoring, simple recommenders, churn flags — these are often linear models, gradient-boosted trees, or tiny networks. For those, Pattern A is over-engineering. The model is a few hundred kilobytes of numbers, and you can run it where the data already is: inside the PHP request. That is what we did.
Why a linear model ports to PHP so easily
The detector is TF-IDF + logistic regression. Once it is trained, scoring a review is not "machine learning" in any heavy sense — it is a weighted sum. No matrix library, no runtime, no Python.
So the integration trick is simply moving the numbers. Export everything PHP needs from the trained scikit-learn pipeline into a small JSON file: each vocabulary term mapped to its IDF weight and model coefficient, plus the intercept and a couple of preprocessing flags.
{
"format": "fakereviews-linear-v1",
"intercept": -7.74,
"params": { "ngram_max": 2, "sublinear_tf": true, "norm": "l2" },
"vocab": {
"love it": [6.43, 2.11], // term -> [idf, coefficient]
"i love": [5.88, 1.40],
"...": [0.0, 0.0]
}
}The export is one command in the Python repo, then a copy into the module:
python -m fakereviews.export_php # -> model/model.json (~900 KB)
cp model/model.json app/code/WisWes/FakeReviews/Model/model.jsonOn the PHP side, scoring is a faithful re-implementation of what scikit-learn does — tokenize the same way, apply the same TF-IDF and L2 normalisation, dot with the coefficients, sigmoid. The core of it is short:
$score = $intercept;
$weights = [];
$norm = 0.0;
// tf-idf weight per in-vocab term (sublinear tf, like sklearn)
foreach ($this->termCounts($text) as $term => $count) {
if (!isset($vocab[$term])) {
continue; // out-of-vocabulary: ignore
}
[$idf, $coef] = $vocab[$term];
$w = (1 + log($count)) * $idf;
$weights[$term] = $w;
$norm += $w * $w;
}
$norm = sqrt($norm);
// L2-normalise, then dot with the coefficients
foreach ($weights as $term => $w) {
$score += ($w / $norm) * $vocab[$term][1];
}
$pFake = 1 / (1 + exp(-$score)); // sigmoid -> probabilityThe detail that earns trust: verify the port against the original. Score the same reviews in Python and in PHP and compare. Ours agrees with scikit-learn to within ~0.0001 — the only gap is rounding in the exported weights. Same verdicts, same explanation tokens. If your PHP port and your training code disagree, the integration is a bug, not a feature.
| Check | Result |
|---|---|
| Max probability difference (PHP vs Python) | ~0.00004 |
| Labels match on the test reviews | 100% |
| Explanation signals match | Yes |
Where it plugs into Magento
With a Classifier service that takes text and returns a probability, the rest is ordinary Magento extension work. One model, reused from three entry points:
The product reviews live under Marketing → All Reviews, which in Magento is still a legacy grid (not a UI component). You cannot add a computed column with a UI-component XML file. The reliable hook is an event that fires just before the grid builds its columns — backend_block_widget_grid_prepare_grid_before — where addColumn() is allowed:
public function execute(Observer $observer): void
{
$grid = $observer->getEvent()->getData('grid');
if (!$grid instanceof \Magento\Review\Block\Adminhtml\Grid) {
return;
}
$grid->addColumn('wiswes_fake', [
'header' => __('Fake?'),
'index' => 'detail', // the review text column
'renderer' => Fake::class, // scores + renders the badge
'filter' => false,
'sortable' => false,
]);
}The column renderer pulls the review text off the row, calls the classifier, and prints a coloured verdict badge — green real 5% or red FAKE 94% — with the words that drove the score in the hover title. The grid is paginated, so only the ~20 rows on screen are scored per page load: in-process inference makes that free.
The same service backs a CLI command, handy for a one-off audit of the whole table:
bin/magento wiswes:fakereviews:scan # flag at the configured threshold
bin/magento wiswes:fakereviews:scan -t 0.35 # higher recall, per runAnd a small Stores → Configuration section exposes the one knob that matters — the probability threshold above which a review is flagged — so a merchant tunes sensitivity without touching code. The grid column and the CLI both read it.
The module, at a glance
Nothing here is exotic Magento. If you have shipped a module, this is the familiar furniture — the only unusual file is the model itself:
| Piece | Role |
|---|---|
Model/model.json | The exported weights — the actual AI, ~900 KB |
Model/Classifier.php | Pure-PHP inference: text in, probability + signals out |
Observer + Block/.../Renderer | Adds and renders the "Fake?" grid column |
Console/Command | The bin/magento scan command |
etc/system|config|acl.xml | The enable toggle + threshold setting |
When you should reach for a service instead
- The model needs a GPU or hundreds of MB of weights — don't try to run a transformer in a PHP request.
- The runtime only exists in another language with no faithful port (a heavy framework, custom CUDA kernels).
- You need to retrain or swap models constantly and want one deploy surface — a service centralises that.
For everything that fits in memory and scores in microseconds, in-process wins: no network, no extra service, no per-call bill, and it works on a merchant's server with nothing else installed.
What's next
The detector now lives where the reviews are. Part 3 takes the same exported model to Shopify, where the integration shape is different — an app and webhooks rather than an in-process PHP module — and the in-process-vs-service tradeoff plays out all over again.
Both pieces are open source: the model and exporter at github.com/wiswes/fakereviews, and the Magento extension at github.com/wiswes/fakereviews_magento.
WisWes builds AI that runs inside your store — answering shoppers, recommending products, and keeping your reviews honest. This series is us building one piece of it in the open.