diff --git a/.php_cs.dist b/.php_cs.dist
deleted file mode 120000
index f23a608677e448362579a78309210a2dac1d534c..0000000000000000000000000000000000000000
--- a/.php_cs.dist
+++ /dev/null
@@ -1 +0,0 @@
-devtools/php/php_cs.dist
\ No newline at end of file
diff --git a/.php_cs.dist b/.php_cs.dist
new file mode 100644
index 0000000000000000000000000000000000000000..5166747b306d2eeea03e8a54073f5a68271913e8
--- /dev/null
+++ b/.php_cs.dist
@@ -0,0 +1,150 @@
+<?php
+define('COMMIT_CACHE', '.php_cs.commit-cache');
+
+$finder = PhpCsFixer\Finder::create()
+    ->exclude('vendor')
+    ->exclude('node_modules')
+    ->exclude('cache')
+    ->files()
+    ->name('*.php')
+    ->in('.');
+
+$yearRange = getGitCommitYears();
+
+$ruleSets = ['@PSR2' => true, '@Symfony' => true];
+
+$rules = [
+    // Configuration && overrides
+    'binary_operator_spaces'                    => ['align_double_arrow' => true],
+    'blank_line_after_opening_tag'              => false,
+    'concat_space'                              => ['spacing' => 'one'],
+    'method_argument_space'                     => ['ensure_fully_multiline' => true],
+
+    // Risky
+    'is_null'                                   => ['use_yoda_style' => false],
+    'non_printable_character'                   => ['use_escape_sequences_in_strings' => true],
+
+    // Safe
+    'align_multiline_comment'                   => true,
+    'array_syntax'                              => ['syntax' => 'short'],
+    'general_phpdoc_annotation_remove'          => ['annotations' => ['author', 'package']],
+    'no_multiline_whitespace_before_semicolons' => true,
+    'no_useless_else'                           => true,
+    'no_useless_return'                         => true,
+    'ordered_imports'                           => true,
+    'phpdoc_add_missing_param_annotation'       => true,
+    'phpdoc_annotation_without_dot'             => true,
+    'phpdoc_order'                              => true,
+    'semicolon_after_instruction'               => true,
+    'yoda_style'                                => false,
+    'self_accessor'                             => false,
+
+    'header_comment' => [
+        'commentType' => 'comment',
+        'location'    => 'after_declare_strict',
+        'separate'    => 'bottom',
+        'header'      => <<<HEADER
+irstea/file-upload-bundle provides services and widgets to manage user-submitted files.
+Copyright (C) $yearRange IRSTEA
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Lesser General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option) any
+later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License and the GNU
+Lesser General Public License along with this program. If not, see
+<https://www.gnu.org/licenses/>.
+HEADER
+        ,
+    ],
+];
+
+$phpVersion = findComposerPhpReq();
+
+if ($phpVersion >= 5.6) {
+    $ruleSets['@PHP56Migration'] = true;
+}
+if ($phpVersion >= 7.0) {
+    $ruleSets['@PHP70Migration'] = true;
+    $rules['declare_strict_types'] = true;
+}
+if ($phpVersion >= 7.1) {
+    $ruleSets['@PHP71Migration'] = true;
+}
+
+echo "Rulesets: " . implode(', ', array_keys($ruleSets)) . ".\n";
+
+return PhpCsFixer\Config::create()
+  ->setRiskyAllowed(true)
+  ->setUsingCache(false)
+  ->setIndent('    ')
+  ->setLineEnding("\n")
+  ->setRules(array_merge($ruleSets, $rules))
+  ->setFinder($finder);
+
+/**
+ * @return string
+ */
+function getGitCommitYears(): string
+{
+    return getCachedValue('years', function () {
+        echo "Examining git history...\n";
+        $last = date('Y');
+        $first = exec('git log --format=%cd --date=format:%Y --date-order | tail -n1') ?? $last;
+        return (null !== $last && $last !== $first) ? "$first-$last" : $first;
+    });
+}
+
+/**
+ * @return float
+ */
+function findComposerPhpReq()
+{
+    return getCachedValue('php-req', function () {
+        if (file_exists('composer.json')) {
+            $data = json_decode(file_get_contents('composer.json'), true);
+            if (is_array($data) && isset($data['require']['php'])) {
+                if (preg_match('/(?:>=?|\^|~)\s*([57]\.[0-9])/', $data['require']['php'], $groups)) {
+                    return (float) $groups[1];
+                }
+            }
+        }
+
+        return 5.6;
+    });
+}
+
+/**
+ * @param string   $key
+ * @param callable $produce
+ *
+ * @return string|int|bool|array
+ */
+function getCachedValue($key, $produce)
+{
+    static $commit = null;
+    if (null === $commit) {
+        $commit = trim(`git rev-parse HEAD`);
+    }
+    if (file_exists(COMMIT_CACHE) && filemtime(COMMIT_CACHE) >= filemtime(__FILE__)) {
+        $cache = json_decode(file_get_contents(COMMIT_CACHE), true);
+    } else {
+        $cache = [];
+    }
+    if (!isset($cache[$commit][$key])) {
+        if (!isset($cache[$commit])) {
+            $cache[$commit] = [];
+        }
+        $cache[$commit][$key] = $produce();
+        file_put_contents(COMMIT_CACHE, json_encode($cache));
+    }
+
+    return $cache[$commit][$key];
+}
+
+// vim:filetype=php