# Tom Girou — Senior Lead Web Developer > Senior Lead Dev — PHP, Symfony & PrestaShop. I help companies cut backlogs from 200+ tickets to under 50 and finally breathe between sprints. Canonical URL: https://tom-girou.dev/ Last updated: portfolio 2026 ## Identity - Full name: Tom Girou - Role: Senior Lead Web Developer - Current employer: Evolutive (https://www.evolutive.com/) - Based in: France (Loire region — 45.4159939° N, 4.5159721° E) - Working languages: English, French, Spanish - LinkedIn: https://www.linkedin.com/in/tgirou - GitHub: https://github.com/Kaikina - X: https://x.com/kaikina_art - YouTube: https://www.youtube.com/@Kaikina - Reddit: https://www.reddit.com/user/Kaikina/ - PrestaShop forum: https://forum.prestashop.com/profile/1776632-tom-girou/ - Personal site: https://tom-girou.dev/ - Availability: open to senior & lead roles, freelance briefs, and rescue missions ## Positioning Turning sprawling backlogs into shippable apps — one cleanly factored module at a time. Hi, I'm Tom — a Senior Lead Dev who treats clean code as the shortest path between a brief and a shipped feature. Stats: - 7+ years of experience - 10+ shipped projects - 8+ clients served ## Core Stack A pragmatic stack — PHP at the core, JavaScript at the surface, and the right glue in between. No buzzwords, just tools that ship. Primary tools: - PHP - MySQL - Postgres - Symfony - JavaScript - SCSS Extended toolbelt: Java, Spring Boot, Angular, AWS, Docker, CI/CD, REST API design, ERP integration, WordPress, WP-CLI, TYPO3, Ruby, Perl, Git, Agile estimation, backlog triage, code review, web application security. ## Selected Work A decade of e‑commerce platforms, gift-card systems, and ERP-bound systems — each one shipped, audited, and quietly humming in production. ### 01. Tactical Sports Marketplace — DG-Airsoft Live site: https://www.destockage-games.com/ Stack: PrestaShop, PHP ### 02. Footwear E-commerce — Les Tropéziennes Live site: https://www.lestropeziennes.fr/fr/ Stack: PrestaShop, PHP ### 03. Collectibles Marketplace — Sylvanian Families Live site: https://sylvanianfamilies-boutique.fr/ Stack: PrestaShop, PHP ### 04. Cakes Storefront — Maison Colibri Live site: https://www.maison-colibri.com/fr/ Stack: PrestaShop, PHP ### 05. Wellness & Herbalism Shop — Herbiolys Live site: https://www.herbiolys.fr/fr/ Stack: PrestaShop, PHP ### 06. E-commerce Suite — Thermcross Live site: https://www.thermcross.fr/fr/ Stack: PrestaShop, PHP ### 07. Gift Card Platform — Wedoogift · Pluxee Stack: Java, Spring Boot, Angular ### 08. WordPress Site Generator — Wedoogift · Pluxee Stack: WordPress, PHP, Bash ## Experience From startup to web agencies. Each role sharpened a different muscle — from cloud infra and CI/CD to leading PrestaShop core integrations. ### 2023 — Now — PHP Developer · Tech Lead at Evolutive Technical leadership and full-stack PrestaShop core & module development. Resource planning, Agile estimations, robust API/ERP integrations, and CI/CD quality assurance. ### 2022 — 2023 — PHP Developer at Thermcross Full-stack e-commerce and backend development specializing in PrestaShop integration, custom PHP/PIM applications, and automated ERP data-sync pipelines — all under Agile. ### 2020 — 2022 — R&D Engineer at Emagineurs Full-stack development, API integration and security engineering across TYPO3, WordPress, and Ruby — specializing in financial tools, automation, and web application security. ### 2018 — 2019 — Fullstack Web Developer at Wedoogift Full-stack & DevOps using Angular, Spring Boot, and AWS — built CI/CD pipelines, shipped under Agile, and authored the documentation that scaled with the team. ## Open Source Beyond client work, I give back to the platform I build on every day — patching core, reviewing PRs, and helping move PrestaShop forward in the open. - PrestaShop core contributor as @Kaikina: https://github.com/PrestaShop/PrestaShop/commits?author=Kaikina - Project repository: https://github.com/PrestaShop/PrestaShop ## Certifications Two official PrestaShop Expert certifications — Core for backend and module work, Front for theme and storefront work. Both verifiable on Procertif. - PrestaShop Expert — Core Skills — issued by PrestaShop, verify on Procertif: https://app.procertif.com/parchment/2511TOMGIR400/check - PrestaShop Expert — Front Skills — issued by PrestaShop, verify on Procertif: https://app.procertif.com/parchment/2602TOMGIR716/check ## FAQ The questions that come up most often when someone is considering working with me — answered straight, in my own voice. Translations available at https://tom-girou.dev/fr/#faq (French), https://tom-girou.dev/es/#faq (Spanish) and https://tom-girou.dev/pt/#faq (Portuguese). ### What does Tom Girou do? I'm a Senior Lead Web Developer based in France, with 7+ years across PHP, Symfony, and PrestaShop. I help e-commerce companies rescue stalled migrations, integrate ERPs, build custom modules, and bring sprawling backlogs back under control. I'm currently at Evolutive in France; recent client work is listed on this site. ### What's your background? I started as a helpdesk agent at Wedoogift (now Pluxee) in 2016 while finishing my development degree, then moved into full-stack Java / Spring Boot / Angular work there as a work-study developer. After graduating I joined Emagineurs as R&D engineer (TYPO3, WordPress, Ruby, security), then Thermcross as in-house PHP developer on a large-scale PrestaShop migration and ERP integration. Since 2023 I've been at Evolutive as PHP Developer and Tech Lead, focused on PrestaShop core and module work for e-commerce clients. ### Do you specialize in PrestaShop? Yes — PrestaShop is where I spend most of my time, and it's the stack I'd point to first if you're deciding whether I'm the right fit. I work at the core and module level across 1.7, 8, and the current 9 release: custom modules, multishop setups, ERP and third-party integrations, theme work, and the performance and security passes a store accumulates as it grows. I also contribute to PrestaShop core itself under @Kaikina on GitHub, mostly fixing the friction I hit in client work rather than chasing feature tickets. That two-way view — building on top of PrestaShop all day and occasionally patching the thing underneath — is what tends to make the difference on the awkward problems. Most of the projects listed on this site are PrestaShop builds: airsoft, footwear, apparel, collectibles, herbal medicine, HVAC parts. If your project is PrestaShop, it's squarely what I do. ### Can you handle PrestaShop migrations (1.5 → 1.7 → 8 → 9)? Yes — it's some of the work I'm asked for most. I've shipped multi-step PrestaShop migrations across several real projects: large multi-shop migrations, major-version upgrades that needed deep core-level work, and stretches where a legacy install had to keep running in production while its replacement was built alongside it. The same approach carries into PrestaShop 9, the current major release. The version bump itself is rarely the hard part. What eats the time is everything hanging off the shop — third-party and custom modules that assume the old APIs, an ERP sync that has to keep matching orders and stock the whole way through, theme overrides written against internals that moved, and years of business rules buried in the database that nobody documented. So I plan a migration around those risks first and treat the core upgrade as the part that goes to plan, because it usually does and they usually don't. ### What about ERP integrations? Most of the PrestaShop work I do touches an ERP or some third-party system sooner or later, so integration is a core part of the job rather than a side quest. I've connected storefronts to ERPs, market-data feeds, and support tooling, and in practice the integration takes one of three shapes: synchronous webservice calls when the answer has to be live, scheduled cron-driven batches when a nightly reconcile is enough, and queued asynchronous pipelines when the volume is high or the third party is slow and a customer can't be left waiting on it. Picking the wrong shape is where these projects usually hurt — a synchronous call that ties checkout to a flaky ERP, or a batch that quietly drifts out of sync between runs. So the real work isn't writing the connector; it's matching the pattern to the data volume, the SLAs, and how much staleness the business can actually tolerate. I've shipped all three. ### Do you work with non-PrestaShop stacks? Yes. Outside PrestaShop I've shipped production work in Symfony, Java with Spring Boot, AngularJS and modern Angular, Perl (data integration), Slim Framework (greenfield PIM), WordPress with WP-CLI and AWS CloudFormation, TYPO3, and Ruby. AWS infrastructure has been part of several projects — EC2, RDS, S3, CloudFormation, Route 53, Certificate Manager — and I've configured CI/CD pipelines on Jenkins, GitLab, and similar. ### Are you available for freelance briefs or new roles? Yes — I'm open to senior and lead roles, freelance briefs, and the occasional rescue mission on a stalled PrestaShop project. The fastest way to start a conversation is LinkedIn; I read every message. I can engage on short or long missions, remote-first, with on-site possible for the right scope. A typical engagement starts with a 30-minute call to understand the scope and the existing state of the codebase. ### Where are you based and what languages do you work in? I'm based in France, in the Loire region (between Lyon and Saint-Étienne). I work in French (native), English (fluent, daily for technical and business communication), and Spanish (working professional, comfortable for written content). Remote-first across France, the EU, and remote-friendly time zones. On-site is possible only around the Lyon / Saint-Étienne area. ### What's the fastest way to reach you? LinkedIn — linkedin.com/in/tgirou — is the fastest path. I read every message and reply within a few working days. If LinkedIn isn't your channel, my GitHub is github.com/Kaikina, and my personal site (tom-girou.dev) has the latest on my work. For sensitive matters, ask for an email address through LinkedIn and we'll move from there. ### Do you contribute to PrestaShop open source? Yes — I contribute to PrestaShop core under @Kaikina on GitHub. Most of my contributions land where I find friction in client work: bug fixes around invoice numbering concurrency, theme cache issues in multishop, and the kind of long-standing edge cases that aren't worth a full ticket for one team but matter for everyone running PrestaShop. The commit history lives at github.com/PrestaShop/PrestaShop/commits?author=Kaikina. ## Blog Notes from the trenches on PHP, Symfony, PrestaShop, CI/CD, and shipping production software — by Tom Girou, Senior Lead Dev. Feed: https://tom-girou.dev/rss.xml ### Three Strings and Two Fallbacks: Wiring the Native Share Sheet Into Forgemage.net Published: 2026-08-27 · URL: https://tom-girou.dev/blog/forgemage-native-share-sheet/ Tags: JavaScript, Web Share API, Symfony, Twig, Progressive Enhancement The share button on Forgemage.net's request detail page opens the OS share sheet wherever the operating system has one — Android, iOS, Windows, macOS — copies the link where it doesn't, and stays quiet when you simply change your mind. Here's the whole implementation: navigator.share with three strings, an AbortError guard that falls through instead of catching, a clipboard fallback with an execCommand fallback under it, and the one-letter Symfony detail that decides whether the shared link works at all. Every mobile app has that button. You tap it, a tray slides up from the bottom of the screen, and there's WhatsApp, Messages, AirDrop, Notes, Discord, whatever you happen to have installed. For years that tray was native-only territory, and the web's answer was a row of brand icons opening `twitter.com/intent/tweet` in a new tab. The tray is available to web pages now. `navigator.share()` opens the exact same OS sheet, and the happy path is six lines. I shipped it last week on [Forgemage.net](https://forgemage.net), the Dofus smithmagie marketplace I keep writing about after [taking the N+1 out of its search page](/blog/forgemage-search-n1-queries/). A smithmage opens a magging request, wants to send it to a friend who might take the job, and until last week the only way to do that was selecting the URL bar on a phone. Now there's a Share button next to Archive. The six lines were the easy part. What took the afternoon was everything around them: where the URL comes from, what happens when someone swipes the sheet away, and what the button should do in a browser whose OS has no share sheet at all. ## The API is three strings and a promise **What does the browser actually hand you?** Not much, and that's the appeal: ```js await navigator.share({ title: 'Magging Request Details', text: 'Someone wants a Gelano maged', url: 'https://forgemage.net/request/01JQ...', }); ``` Three optional strings, plus a `files` array. No SDK, no app ID, no OAuth dance, no script tag from a company that would like to A/B test my page for me. Two hard requirements, though. The page has to be a secure context, so HTTPS in production and localhost while you work. And the call has to happen inside a real user gesture. Not on `DOMContentLoaded`, not in a `setTimeout`, and — this is the one that catches people — not after an `await` that goes to the network. Fetch a short link from your own API and *then* call `share()`, and the transient activation is gone and the browser rejects you. Whatever you're going to share has to be sitting in memory before the click. ## Where the URL comes from decides whether the link works **Why not just share `location.href`?** Because on Forgemage.net the address bar isn't reliably the thing worth sending. The request detail route is localized, so the same page lives at `/request/{ulid}` in English, `/requete/{ulid}` in French and `/pedido/{ulid}` in Spanish. Several of the interesting entry points are dashboard views whose visible URL carries query state nobody else needs. So the URL comes from the server, in the markup: ```twig ``` The important character in there is the `u` in `url()`. Symfony's `path()` generates `/request/01JQ…`, which is perfect for an `href` and useless in a share payload: pasted into Discord it isn't a link, and a native target has no idea what host to prepend. `url()` gives the absolute form. One letter between a working feature and a bug report that says "the link doesn't open". The rest of the attributes exist because JavaScript can't reach the Twig translator. The label, the confirmation string and the share title are rendered server-side into `data-*` and read back at click time. Slightly ugly, and the alternative is shipping a translation catalogue to the client so a button can say "Copied" in three languages. The ULID is doing quiet work too. The route is `/{ulid:fmRequest}`, so shared links aren't guessable the way sequential ids are. Nobody's going to walk `/request/1`, `/request/2` and read the whole table because I put a share button on the page. ## One fallthrough handles every failure **What happens when the sheet doesn't open?** This is the entire share handler, and its shape is the only real decision in the file: ```js async function share(button) { const url = button.dataset.shareUrl || window.location.href; if (navigator.share) { try { await navigator.share({ title: button.dataset.shareTitle || document.title, url }); return; } catch (error) { if (error.name === 'AbortError') { return; } } } if (await copyToClipboard(url)) { confirmCopy(button); } } ``` Two `return`s and no `else`. The success path returns, the cancel path returns, and everything else falls out of the `if` and lands on the clipboard: no `navigator.share` at all, a `NotAllowedError` from a lost gesture, a `DataError` from a URL that doesn't parse, a Permissions Policy blocking the call inside an iframe. I never had to enumerate the failures. I only had to name the two cases where doing nothing is the correct behaviour. `AbortError` is the one worth being careful about. It's what you get when the user swipes the sheet away, and it arrives as a rejected promise looking exactly like a real problem. Catch it generically and you show "Sharing failed" to somebody who simply changed their mind, which is the kind of small thing that makes software feel like it isn't paying attention. Cancelling is a successful outcome of a share sheet. It just isn't a share. The other quiet win is desktop, and it isn't the one I expected. The share sheet isn't a phone feature: Chrome and Edge on Windows hand the payload to the Windows share flyout, and Safari on macOS opens the same sheet any Mac app gets. What decides it is whether the OS has a share UI and the browser bothers to wire it up. Linux doesn't have one, so `navigator.share` is undefined and my own Chrome copies the link instead — the fallback was the only path I saw for the first hour of building this. Firefox on desktop doesn't implement it anywhere. All of that is one `if` and no feature flag: the markup ships one button and each browser decides what it means. ## The fallback has a fallback **Why keep `document.execCommand('copy')` around in 2026?** Because `navigator.clipboard` needs a secure context and a permission that can be refused, and a copy button that silently does nothing is worse than no copy button: ```js async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch { const field = document.createElement('textarea'); field.value = text; field.setAttribute('readonly', ''); field.style.position = 'fixed'; field.style.opacity = '0'; document.body.append(field); field.select(); const copied = document.execCommand('copy'); field.remove(); return copied; } } ``` The off-screen textarea trick is deprecated, works nearly everywhere, and returns a boolean I can act on. Note the `opacity: 0` with `position: fixed` rather than `display: none` or a negative offset: the field has to be selectable, so it can be invisible but it can't be gone, and it shouldn't scroll the page on the way in. It also has to come out in the same tick, otherwise a slow frame leaves a real focusable element in the DOM for a screen reader to find. Both paths return a boolean rather than throwing, which is why the caller reads as one clean `if`. ## The button has to answer **How does the user know anything happened?** On the native path they know, because the OS drew a sheet over the page. On the clipboard path nothing visible happens at all, so the button says it itself: ```js function confirmCopy(button) { const label = button.querySelector('.rd-share__label'); const icon = button.querySelector('i'); const original = label.textContent; button.classList.add('is-copied'); label.textContent = button.dataset.copiedLabel || 'Copied'; icon.className = 'bi bi-check2'; setTimeout(() => { button.classList.remove('is-copied'); label.textContent = original; icon.className = 'bi bi-share-fill'; }, 1800); } ``` Icon becomes a checkmark, label becomes the translated "Copied", both go back after 1.8 seconds. No toast library, no notification container, no state store. The feedback lives on the element the user just touched, which is where their eyes already are. Reading `original` off the DOM instead of hardcoding "Share" is what makes the restore correct in French and Spanish. And 1800 is a number I picked because 1000 felt clipped and 3000 felt like the button was stuck. ## What I left out on purpose **Why is there no `text`, and no image?** The payload is `title` and `url`, nothing else, and that's deliberate. The spec is explicit that all three fields are hints: the receiving app decides what to do with them, and several targets take exactly one of `text` or `url` and drop the other on the floor. Passing both is how you end up with a message that describes a magging request without linking to it. If the link is the point, send the link. `title` is mostly ignored too. Android Chrome uses it as the subject line for mail targets and discards it elsewhere. I pass it because when it does get read it's the right string, and when it doesn't, nothing is lost. Files would be the obvious next step, since a request detail page has an item and a stat line that would screenshot nicely: ```js if (navigator.canShare?.({ files: [file] })) { await navigator.share({ files: [file] }); } ``` `canShare()` is the only honest way to find out whether the platform accepts that MIME type and that size, and it shipped later than `share()` itself, hence the optional call. I skipped all of it. Rendering an image server-side, prefetching it before the click so the user gesture survives, and accepting that several platforms ignore `url` when `files` is present is a feature, not a fallback. Different afternoon. Same for the other direction. A PWA can register itself *inside* the sheet with `share_target` in its manifest and receive a plain multipart POST, which any Symfony controller already knows how to read. Android only, nothing on iOS, and Forgemage.net isn't installable yet. ## The bits I'm not proud of **What's still owed?** The shared link hits a login wall. `RequestController::detail()` calls `denyAccessUnlessGranted('ROLE_USER')`, so a friend who taps the link without an account lands on a login page instead of the request. Correct for the data, rough as a first impression for a link somebody chose to send. A public teaser view would fix it and I haven't built one. The locale rides along. `url()` generates the path for the sharer's locale, so a French smithmage sharing to a Spanish friend sends them `/requete/…`, and the app serves it in French. Sharing arguably wants a locale-neutral canonical URL. I send the localized one because that's what the route helper gives you without thinking about it. The button markup exists twice in `detail.html.twig`, once in each branch of the owner/visitor condition, all three `data-*` attributes duplicated. It should be a component or an include taking the request. It's a copy-paste, and I noticed it while writing this paragraph rather than while writing the template. And I have no idea whether anyone uses it. No event, no counter, nothing. The honest version of this post opens with a number instead of a story. ## Takeaways 1. **The gesture is the constraint, not the API.** `navigator.share()` is three strings, and the main way to get it wrong is to `await` something else first. Build the payload before the click, or share a URL you already have. 2. **`AbortError` means "no, thanks".** Treat a dismissed sheet as a failure and your app tells people off for changing their mind. Two `return`s in the right place, and every other failure can share one fallback. 3. **Absolute URLs or nothing.** In Symfony that's `url()`, not `path()`. A relative link in a share payload stops being a link the moment it leaves the browser. 4. **Feature-detect to reshape the button, not to hide it.** One `