Building PHP Ukraine with Laravel and PostgreSQL
A couple of weeks ago I started building PHP Ukraine . The initial idea was pretty simple. I wanted one place where PHP developers in Ukraine could find jobs, companies, interview questions, documentation and useful technical content. Then, as usual, the project started growing. I added job aggregation from several sources, classification, deduplication, salary normalization, search, employer…
A PHP Ukraine project was initiated with the goal of providing a centralized platform for Ukrainian PHP developers to discover job opportunities, companies, interview questions, documentation and useful technical content. As the project evolved, additional features were added such as job aggregation from multiple sources, classification, deduplication, salary normalization, search, employer tools, analytics, content processing, and more.
The chosen technology stack includes PHP 8.4, Laravel 13, PostgreSQL 17, Livewire, Filament, and Docker with FrankenPHP.
The codebase is organized into separate contexts under the src/ directory, each containing Domain, Application, and Infrastructure layers. This modular approach aims to maintain clear boundaries and prevent the project from becoming overly complex. Laravel handles the framework aspects, with controllers, Livewire components, console commands, middleware, and service providers located in the app/ directory.
To enforce these boundaries, architecture tests using Pest have been implemented. These tests ensure that the domain layers remain framework-free and that the web layer does not directly interact with infrastructure implementations. This results in a more maintainable and organized codebase.
The job aggregation component currently imports vacancies from Robota.ua, DOU, and Djinni. Each source follows the same JobSource interface and returns normalized job objects. Robota.ua provides jobs via JSON API endpoints, while DOU and Djinni are fetched through RSS feeds. To handle Djinni's incomplete RSS feed, the app fetches the vacancy page and reads the JobPosting JSON-LD to gather additional details.
The job aggregation process involves fetching jobs, ignoring unwanted employers and non-PHP jobs, updating existing jobs if a source reference already exists, checking for duplicate jobs from other sources, fetching extra details when needed, and creating new jobs. Stale jobs are marked accordingly. Job aggregation runs hourly, with deduplication, expiration, salary exchange rate adjustments, and market snapshots occurring separately.
The classification system relies on rules rather than AI. It aims to detect framework, seniority, city, work format, employment type, English level, salary, and technology tags. Salary parsing can handle various formatting, such as $3000-4000, €4000, or 80k UAH, and normalizes the salary to USD for filtering and sorting purposes. Exchange rates are sourced from the National Bank of Ukraine.
Deduplication across job boards is handled by generating a fingerprint based on the company name and title. The process normalizes the company name, removes noise, and cleans up the title before comparison. If a vacancy appears on multiple sources, the primary record is kept, while additional source URLs are attached to it. If a board republishes the vacancy with a new ID, this is managed separately.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.