diff --git a/config/routes.yaml b/config/routes.yaml index 9376996e..8ce1513e 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -3,7 +3,7 @@ afup_barometre_homepage: methods: GET controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController defaults: - path: /about + path: /report/2025 permanent: false afup_barometre_form: diff --git a/config/services.yaml b/config/services.yaml index 28955e33..d6812b5a 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -282,6 +282,14 @@ services: parent: App\Report\AbstractReport tags: ['barometre.report'] + App\Report\UseGenerativeAiReport: + parent: App\Report\AbstractReport + tags: ['barometre.report'] + + App\Report\IncludeAiInProjectReport: + parent: App\Report\AbstractReport + tags: [ 'barometre.report' ] + NumberFormatter: class: \NumberFormatter arguments: ['fr', '1'] diff --git a/migrations/Version20250906084617.php b/migrations/Version20250906084617.php new file mode 100644 index 00000000..c8dc01ea --- /dev/null +++ b/migrations/Version20250906084617.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE response ADD useGenerativeAI TINYINT(1) DEFAULT NULL, ADD includeAiInProject TINYINT(1) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE response DROP useGenerativeAI, DROP includeAiInProject'); + } +} diff --git a/migrations/Version20251002080532.php b/migrations/Version20251002080532.php new file mode 100644 index 00000000..40793896 --- /dev/null +++ b/migrations/Version20251002080532.php @@ -0,0 +1,30 @@ +addSql('INSERT INTO speciality (name) VALUES(:name)', ['name' => $speciality]); + } + } +} diff --git a/public/reports/2025/comparatif_avec_inflation.png b/public/reports/2025/comparatif_avec_inflation.png new file mode 100644 index 00000000..e5174743 Binary files /dev/null and b/public/reports/2025/comparatif_avec_inflation.png differ diff --git a/public/reports/2025/evolution_salaire.png b/public/reports/2025/evolution_salaire.png new file mode 100644 index 00000000..6f922f8c Binary files /dev/null and b/public/reports/2025/evolution_salaire.png differ diff --git a/public/reports/2025/evolution_salaire_moyen_genre.png b/public/reports/2025/evolution_salaire_moyen_genre.png new file mode 100644 index 00000000..f83040c3 Binary files /dev/null and b/public/reports/2025/evolution_salaire_moyen_genre.png differ diff --git a/public/reports/2025/ia_usage_in_project.png b/public/reports/2025/ia_usage_in_project.png new file mode 100644 index 00000000..d5248eea Binary files /dev/null and b/public/reports/2025/ia_usage_in_project.png differ diff --git a/public/reports/2025/nombre_reponse_genre.png b/public/reports/2025/nombre_reponse_genre.png new file mode 100644 index 00000000..ba4c7c7f Binary files /dev/null and b/public/reports/2025/nombre_reponse_genre.png differ diff --git a/public/reports/2025/repartition_teletravailleurs.png b/public/reports/2025/repartition_teletravailleurs.png new file mode 100644 index 00000000..4cebe050 Binary files /dev/null and b/public/reports/2025/repartition_teletravailleurs.png differ diff --git a/public/reports/2025/salaire_median_euros_constants.png b/public/reports/2025/salaire_median_euros_constants.png new file mode 100644 index 00000000..f4b04453 Binary files /dev/null and b/public/reports/2025/salaire_median_euros_constants.png differ diff --git a/public/reports/2025/use_generative_ai.png b/public/reports/2025/use_generative_ai.png new file mode 100644 index 00000000..ca0e3b69 Binary files /dev/null and b/public/reports/2025/use_generative_ai.png differ diff --git a/src/Campaign/Format/Formats/Format2019.php b/src/Campaign/Format/Formats/Format2019.php index 2992da0a..4d13dc80 100644 --- a/src/Campaign/Format/Formats/Format2019.php +++ b/src/Campaign/Format/Formats/Format2019.php @@ -56,8 +56,8 @@ public function alterData(array $data) } if ('Freelance / entreprise individuelle' === $data['status'] - && '' !== trim($data['freelance_tjm']) - && '' !== trim($data['freelance_average_work_day']) + && '' !== mb_trim($data['freelance_tjm']) + && '' !== mb_trim($data['freelance_average_work_day']) ) { // calcul basic du brut d'un freelance $freelanceAnnualSalary = $data['freelance_tjm'] * $data['freelance_average_work_day']; diff --git a/src/Campaign/Format/Formats/Format2025.php b/src/Campaign/Format/Formats/Format2025.php new file mode 100644 index 00000000..cf1538ef --- /dev/null +++ b/src/Campaign/Format/Formats/Format2025.php @@ -0,0 +1,87 @@ +setUseGenerativeAI( + 'oui' === mb_strtolower($data['use_generative_ai'] ?? null) + ); + + $response->setIncludeAiInProject( + 'oui' === mb_strtolower($data['include_ai_in_project'] ?? null) + ); + return $response; } @@ -202,7 +210,7 @@ protected function addCertification(Response $response, array $certificationList foreach ($certificationList as $certification) { $certification = $this->certificationRepository->findOneBy( [ - 'name' => trim($certification), + 'name' => mb_trim($certification), ] ); @@ -217,7 +225,7 @@ protected function addCertification(Response $response, array $certificationList protected function addSpeciality(Response $response, array $specialityList) { foreach ($specialityList as $speciality) { - $speciality = $this->specialityRepository->findOneBy(['name' => trim($speciality)]); + $speciality = $this->specialityRepository->findOneBy(['name' => mb_trim($speciality)]); if (!$speciality instanceof Speciality) { continue; @@ -230,7 +238,7 @@ protected function addSpeciality(Response $response, array $specialityList) private function addHostingType(Response $response, array $hostingType) { foreach ($hostingType as $hostingTypeName) { - $hostingType = $this->hostingTypeRepository->findOneBy(['name' => trim($hostingTypeName)]); + $hostingType = $this->hostingTypeRepository->findOneBy(['name' => mb_trim($hostingTypeName)]); if (!$hostingType instanceof HostingType) { continue; @@ -243,7 +251,7 @@ private function addHostingType(Response $response, array $hostingType) private function addContainerEnvironmentUsage(Response $response, array $containerEnvironmentsUsage) { foreach ($containerEnvironmentsUsage as $name) { - $containerEnvironmentUsage = $this->containerEnvironmentUsageRepository->findOneBy(['name' => trim($name)]); + $containerEnvironmentUsage = $this->containerEnvironmentUsageRepository->findOneBy(['name' => mb_trim($name)]); if (!$containerEnvironmentUsage instanceof ContainerEnvironmentUsage) { continue; diff --git a/src/Entity/Response.php b/src/Entity/Response.php index ec18e9db..8531a76b 100644 --- a/src/Entity/Response.php +++ b/src/Entity/Response.php @@ -181,6 +181,12 @@ class Response #[ORM\Column(type: 'integer', nullable: true)] private ?int $age = null; + #[ORM\Column(type: 'boolean', nullable: true)] + private ?bool $useGenerativeAI = null; + + #[ORM\Column(type: 'boolean', nullable: true)] + private ?bool $includeAiInProject = null; + public function __construct() { $this->certifications = new ArrayCollection(); @@ -896,4 +902,28 @@ public function setAge(?int $age): self return $this; } + + public function getUseGenerativeAI(): ?bool + { + return $this->useGenerativeAI; + } + + public function setUseGenerativeAI(?bool $useGenerativeAI): self + { + $this->useGenerativeAI = $useGenerativeAI; + + return $this; + } + + public function getIncludeAiInProject(): ?bool + { + return $this->includeAiInProject; + } + + public function setIncludeAiInProject(?bool $includeAiInProject): self + { + $this->includeAiInProject = $includeAiInProject; + + return $this; + } } diff --git a/src/Enums/AbstractEnums.php b/src/Enums/AbstractEnums.php index ff4b33d6..d237af72 100644 --- a/src/Enums/AbstractEnums.php +++ b/src/Enums/AbstractEnums.php @@ -22,7 +22,7 @@ public function getIds(): array public function getIdByLabel(?string $label): ?int { - $key = array_search(trim($label ?? ''), $this->choices, true); + $key = array_search(mb_trim($label ?? ''), $this->choices, true); return false === $key ? $this->getDefaultValue() : $key; } diff --git a/src/Enums/OuiNonEnums.php b/src/Enums/OuiNonEnums.php new file mode 100644 index 00000000..817dfd90 --- /dev/null +++ b/src/Enums/OuiNonEnums.php @@ -0,0 +1,16 @@ + 'Oui', + self::NO => 'Non', + ]; +} diff --git a/src/Filter/UseGenerativeAIFilter.php b/src/Filter/UseGenerativeAIFilter.php new file mode 100644 index 00000000..5961c593 --- /dev/null +++ b/src/Filter/UseGenerativeAIFilter.php @@ -0,0 +1,69 @@ +add($this->getName(), Select2MultipleFilterType::class, [ + 'label' => 'filter.use_generative_ai', + 'choices' => array_flip($this->enums->getChoices()), + ]); + } + + /** + * {@inheritdoc} + */ + public function buildQuery(QueryBuilder $queryBuilder, array $values = []) + { + if (!\array_key_exists($this->getName(), $values) || 0 === \count($values[$this->getName()])) { + return; + } + + $queryBuilder + ->andWhere($queryBuilder->expr()->in('response.useGenerativeAI', $values[$this->getName()])); + } + + /** + * {@inheritdoc} + */ + public function convertValuesToLabels($value) + { + return array_map(function ($value) { + return $this->enums->getLabelById($value); + }, $value); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'use_generative_ai'; + } + + /** + * Filter weight. + * + * @return int + */ + public function getWeight() + { + return 160; + } +} diff --git a/src/Menu/MenuBuilder.php b/src/Menu/MenuBuilder.php index 9fa47ca1..0342a35f 100644 --- a/src/Menu/MenuBuilder.php +++ b/src/Menu/MenuBuilder.php @@ -41,10 +41,10 @@ protected function getBaseMenu() $menu->setChildrenAttribute('class', 'nav navbar-nav'); $menu->addChild( - 'menu.result2024', + 'menu.result2025', [ 'route' => 'afup_barometre_campaign', - 'routeParameters' => ['campaignName' => 2024], + 'routeParameters' => ['campaignName' => 2025], 'routeAbsolute' => UrlGeneratorInterface::ABSOLUTE_URL, ] ); diff --git a/src/Report/IncludeAiInProjectReport.php b/src/Report/IncludeAiInProjectReport.php new file mode 100644 index 00000000..acb723b1 --- /dev/null +++ b/src/Report/IncludeAiInProjectReport.php @@ -0,0 +1,40 @@ +queryBuilder + ->select('response.includeAiInProject as includeAiInProject') + ->addSelect('COUNT(response.id) as nbResponse') + ->addGroupBy('response.includeAiInProject') + ->addOrderBy('response.includeAiInProject', 'desc'); + + $this->data = $this->queryBuilder->fetchAllAssociative(); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'include_ai_in_project'; + } + + /** + * report weight. + * + * @return int + */ + public function getWeight() + { + return 120; + } +} diff --git a/src/Report/SpecialityEvolutionReport.php b/src/Report/SpecialityEvolutionReport.php index 10baf694..3bc9288e 100644 --- a/src/Report/SpecialityEvolutionReport.php +++ b/src/Report/SpecialityEvolutionReport.php @@ -52,9 +52,14 @@ public function execute() $framework = [ 'Symfony', 'Laravel', - 'Zend Framework', 'Wordpress', 'Drupal', + 'Laminas', + ]; + + $laminas = [ + 'Zend Framework', + 'Laminas (ex Zend Framework)', ]; $otherFramework = 'Autre'; @@ -71,6 +76,10 @@ public function execute() $specialityName = $row['specialityName']; } + if (\in_array($row['specialityName'], $laminas, true)) { + $specialityName = 'Laminas'; + } + if (!isset($data[$row['name']][$specialityName])) { $data[$row['name']][$specialityName] = 0; } diff --git a/src/Report/UseGenerativeAiReport.php b/src/Report/UseGenerativeAiReport.php new file mode 100644 index 00000000..cbcf6fa8 --- /dev/null +++ b/src/Report/UseGenerativeAiReport.php @@ -0,0 +1,40 @@ +queryBuilder + ->select('response.useGenerativeAI as useGenerativeAI') + ->addSelect('COUNT(response.id) as nbResponse') + ->addGroupBy('response.useGenerativeAI') + ->addOrderBy('response.useGenerativeAI', 'desc'); + + $this->data = $this->queryBuilder->fetchAllAssociative(); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'use_generative_ai'; + } + + /** + * report weight. + * + * @return int + */ + public function getWeight() + { + return 100; + } +} diff --git a/templates/Campaign/report2025.html.twig b/templates/Campaign/report2025.html.twig new file mode 100644 index 00000000..60f045b3 --- /dev/null +++ b/templates/Campaign/report2025.html.twig @@ -0,0 +1,150 @@ +{% extends "layout.html.twig" %} + +{% block title %}Résultats de la campagne {{ campaignName }}{% endblock %} + +{% block meta %} + {{ parent() }} + + + + + +{% endblock %} + + +{% block content %} +
+ L’AFUP est ravie de vous présenter les résultats de son enquête effectuée en début d’année auprès d’environ 721 devs. Cette année est marquée par des salaires augmentés du niveau de l’inflation, sur fond d’écart des salaires entre les hommes et les femmes ne se réduisant pas, et sur une forte utilisation de l’IA générative. On vous explique tout cela en détail. +
+ +Comme chaque année l’AFUP effectue une enquête auprès des développeurs et développeuses PHP. Cette enquête a eu lieu du 17 mars au 17 juin 2025.
+ +Cette année nous avons ajouté deux questions liées à l’IA générative dont vous trouverez les résultats dans cette étude. Une page et deux filtres dédiés ont été ajoutés sur le baromètre.
+ +Les personnes ayant répondu :
+Nous pouvons constater sur
+ + +
+
+
+ que les profils de dev/lead tech en CDI ont vu sur les deux dernières années des augmentations de salaire similaires au taux d’inflation (https://www.insee.fr/fr/statistiques/2414942#tableau-figure1) de l’année précédente.
+ +Le salaire médian augmente de +2,17 % en 2025, après +4,55 % en 2024. Ces évolutions suivent de près l’inflation des années précédentes (2 % en 2024 ; 4,9 % en 2023)
+ +Depuis que le baromètre des salaires existe, nous avions plutôt constaté des augmentations de salaire bien plus fortes que l’inflation, comme en 2015, 2017, 2020 et 2021. Depuis 2022 ces augmentations sont équivalentes à l’inflation, à l’exception de 2023 où l’augmentation était inférieure à l’inflation.
+ +
+
+ Si l’on regarde la période de 2013 à 2025, le salaire médian en euros courants a gagné environ 12 000 €. Rapporté en euros constants, le gain net tourne autour de 1 151 €.
+ +
+
+
+ Le salaire moyen des hommes dev/tech lead en CDI est de 53k, il a connu une augmentation de 5,1%, alors que le salaire moyen des femmes dev/tech lead en CDI est de 47k, et a connu une augmentation de 5,6%.
+ +Ces salaires moyens ont augmenté de façon similaire, ce qui fait que l’écart reste toujours le même à travers le temps et, pour l'instant, il n’y a pas de tendance à ce que le salaire des femmes rejoigne celui des hommes.
+ + +
+
+
+
+ Le télétravail s’est installé avec 81 % des répondant·es qui le pratiquent régulièrement ou à temps plein, contre 16 % seulement en 2019. Après le pic de 2020‑2021, on observe un plateau haut depuis 2022.
+ + +
+
+
+ Comme indiqué précédemment nous avons cette année ajouté 2 questions, la première concernait l’usage de l’IA générative pour développer les projets.
+ +Cet usage est fortement répandu parmi les répondants et répondantes. Deux personnes sur trois déclarent utiliser l'IA pour développer leurs projets.
+ +Nous continuerons de suivre l’évolution de cette métrique au cours des prochaines enquêtes.
+ + +
+
+
+ Pour ce qui est de l’intégration de l’IA générative dans les projets, même si celle-ci est bien moindre que l’usage de l’IA générative pour développer les projets, elle est tout de même très forte.
+ +En effet, 28% des projets des personnes interrogées intègrent des fonctionnalités d’IA générative dans leur projets.
+ +Là aussi nous continuerons de suivre cet indicateur à travers le temps.
+ + +
+
+
+
+ Le baromètre des salaires AFUP est un projet open-source porté par des bénévoles. Nous sommes toujours à la recherche de personnes pour aider sur ce projet, que ça soit côté technique, pour le lancement de l’enquête ou l’analyse des résultats. Si vous souhaitez participer vous pouvez envoyer un message à barometre@afup.org.
+ + +L'association française des utilisateurs de PHP (AFUP) et Human Coders vous présentent pour la 13ème année consécutive le baromètre des salaires AFUP – 2025.
+ +Comme chaque année, le baromètre AFUP répond aux questions que se posent les professionnel·le·s de notre secteur :
+Nous espérons que vous aurez une vision claire du marché après consultation de ce baromètre des salaires en PHP.
+ +L'AFUP, Association Française des Utilisateurs de PHP, est une association loi 1901, qui a pour objectif principal de promouvoir le langage PHP auprès des professionnel·le·s et de participer à son développement. Elle organise de nombreux événements tout au long de l'année, notamment le Forum PHP et les AFUP Day, elle diffuse et partage les connaissances auprès des utilisateurs et utilisatrices de PHP, et participe à la valorisation des développeurs et développeuses PHP sur le marché du travail.
+ +Human Coders est un centre de formation pour développeur·euse·s. Fondé par 2 développeurs web il y a 13 ans, il forme sur plus de 150 langages / frameworks / méthodologies dont bien sûr PHP, Symfony et Laravel, mais aussi sur d’autres domaines comme le frontend, l’IA ou encore devops.
+ +Convaincu de l’importance de faire de la veille pour rester à jour, Human Coders, c’est aussi un podcast pour développeur·euse·s, un site d’actualité et des événements mensuels dans toute la France.
+| {{ "report.include_ai_in_project.label" | trans }} | +{{ "report.view.response_number" | trans }} | +
|---|---|
| + {{ row.includeAiInProject|enum_label('App\\Enums\\OuiNonEnums') }} + | +{{ row.nbResponse|number_format(null, '', ' ') }} | +
| {{ "report.use_generative_ai.label" | trans }} | +{{ "report.view.response_number" | trans }} | +
|---|---|
| + {{ row.useGenerativeAI|enum_label('App\\Enums\\OuiNonEnums') }} + | +{{ row.nbResponse|number_format(null, '', ' ') }} | +