diff --git a/.arc/arc-phpstan/__phutil_library_init__.php b/.arc/arc-phpstan/__phutil_library_init__.php
new file mode 100755
--- /dev/null
+++ b/.arc/arc-phpstan/__phutil_library_init__.php
@@ -0,0 +1,18 @@
+<?php
+/*
+ Copyright 2017-present Appsinet. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+phutil_register_library('phpstan', __FILE__);
diff --git a/.arc/arc-phpstan/__phutil_library_map__.php b/.arc/arc-phpstan/__phutil_library_map__.php
new file mode 100755
--- /dev/null
+++ b/.arc/arc-phpstan/__phutil_library_map__.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * This file is automatically generated. Use 'arc liberate' to rebuild it.
+ *
+ * @generated
+ * @phutil-library-version 2
+ */
+phutil_register_library_map(array(
+  '__library_version__' => 2,
+  'class' => array(
+    'PhpstanLinter' => 'lint/linter/PhpstanLinter.php',
+  ),
+  'function' => array(),
+  'xmap' => array(
+    'PhpstanLinter' => 'ArcanistExternalLinter',
+  ),
+));
diff --git a/.arc/arc-phpstan/lint/linter/PhpstanLinter.php b/.arc/arc-phpstan/lint/linter/PhpstanLinter.php
new file mode 100755
--- /dev/null
+++ b/.arc/arc-phpstan/lint/linter/PhpstanLinter.php
@@ -0,0 +1,242 @@
+<?php
+/**
+ * @copyright Copyright 2017-present Appsinet. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+/** Uses phpstan to lint php files */
+final class PhpstanLinter extends ArcanistExternalLinter
+{
+
+    /**
+     * @var string Config file path
+     */
+    private $configFile = null;
+
+    /**
+     * @var string Rule level
+     */
+    private $level = null;
+
+    /**
+     * @var string Autoload file path
+     */
+    private $autoloadFile = null;
+
+    public function getInfoName()
+    {
+        return 'phpstan';
+    }
+
+    public function getInfoURI()
+    {
+        return '';
+    }
+
+    public function getInfoDescription()
+    {
+        return pht('Use phpstan for processing specified files.');
+    }
+
+    public function getLinterConfigurationName()
+    {
+        return 'phpstan';
+    }
+
+    public function getDefaultBinary()
+    {
+        return 'phpstan';
+    }
+
+    public function getInstallInstructions()
+    {
+        return pht('Install phpstan following the official guide at https://github.com/phpstan/phpstan#installation');
+    }
+
+    public function shouldExpectCommandErrors()
+    {
+        return true;
+    }
+
+    public function getVersion()
+    {
+        list($stdout) = execx('%C --version', $this->getExecutableCommand());
+
+        $matches = array();
+        $regex = '/(?P<version>\d+\.\d+\.\d+)/';
+        if (preg_match($regex, $stdout, $matches)) {
+            return $matches['version'];
+        } else {
+            return false;
+        }
+    }
+
+    protected function getMandatoryFlags()
+    {
+        $flags = array(
+            'analyse',
+            '--no-progress',
+            '--error-format=checkstyle',
+            '--memory-limit=1G'
+        );
+        if (null !== $this->configFile) {
+            array_push($flags, '-c', $this->configFile);
+        }
+        if (null !== $this->level) {
+            array_push($flags, '-l', $this->level);
+        }
+        if (null !== $this->autoloadFile) {
+            array_push($flags, '-a', $this->autoloadFile);
+        }
+
+        return $flags;
+    }
+
+    public function getLinterConfigurationOptions()
+    {
+        $options = array(
+            'config' => array(
+                'type' => 'optional string',
+                'help' => pht(
+                    'The path to your phpstan.neon file. Will be provided as -c <path> to phpstan.'
+                ),
+            ),
+            'level' => array(
+                'type' => 'optional string',
+                'help' => pht(
+                    'Rule level used (0 loosest - max strictest). Will be provided as -l <level> to phpstan.'
+                ),
+            ),
+            'autoload' => array(
+                'type' => 'optional string',
+                'help' => pht(
+                    'The path to the auto load file. Will be provided as -a <autoload_file> to phpstan.'),
+            ),
+        );
+        return $options + parent::getLinterConfigurationOptions();
+    }
+
+    public function setLinterConfigurationValue($key, $value)
+    {
+        switch ($key) {
+        case 'config':
+            $this->configFile = $value;
+            return;
+        case 'level':
+            $this->level = $value;
+            return;
+        case 'autoload':
+            $this->autoloadFile = $value;
+            return;
+        default:
+            parent::setLinterConfigurationValue($key, $value);
+            return;
+        }
+    }
+
+    protected function getDefaultMessageSeverity($code)
+    {
+        return ArcanistLintSeverity::SEVERITY_WARNING;
+    }
+
+    protected function parseLinterOutput($path, $err, $stdout, $stderr)
+    {
+        $result = array();
+        if (!empty($stdout)) {
+            $stdout = substr($stdout, strpos($stdout, '<?xml'));
+            $checkstyleOutpout = new SimpleXMLElement($stdout);
+            $errors = $checkstyleOutpout->xpath('//file/error');
+            foreach($errors as $error) {
+                    $violation = $this->parseViolation($error);
+                    $violation['path'] = $path;
+                    $result[] = ArcanistLintMessage::newFromDictionary($violation);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Checkstyle returns output of the form
+     *
+     * <checkstyle>
+     *   <file name="${sPath}">
+     *     <error line="12" column="10" severity="${sSeverity}" message="${sMessage}" source="${sSource}">
+     *     ...
+     *   </file>
+     * </checkstyle>
+     *
+     * Of this, we need to extract
+    *   - Line
+     *   - Column
+     *   - Severity
+     *   - Message
+     *   - Source (name)
+     *
+     * @param SimpleXMLElement $violation The XML Entity containing the issue
+     *
+     * @return array of the form
+     * [
+     *   'line' => {int},
+    *   'column' => {int},
+     *   'severity' => {string},
+     *   'message' => {string}
+     * ]
+     */
+    private function parseViolation(SimpleXMLElement $violation)
+    {
+        return array(
+            'code' => $this->getLinterName(),
+            'name' => (string)$violation['message'],
+            'line' => (int)$violation['line'],
+            'char' => (int)$violation['column'],
+            'severity' => $this->getMatchSeverity((string)$violation['severity']),
+            'description' => (string)$violation['message']
+        );
+    }
+
+    /**
+     * @return string Linter name
+     */
+    public function getLinterName()
+    {
+        return 'phpstan';
+    }
+
+    /**
+     * Map the regex matching groups to a message severity. We look for either
+     * a nonempty severity name group like 'error', or a group called 'severity'
+     * with a valid name.
+     *
+     * @param string $severity_name dict Captured groups from regex.
+     *
+     * @return string @{class:ArcanistLintSeverity} constant.
+     *
+     * @task parse
+     */
+    private function getMatchSeverity($severity_name)
+    {
+            $map = array(
+                    'error' => ArcanistLintSeverity::SEVERITY_ERROR,
+                    'warning' => ArcanistLintSeverity::SEVERITY_WARNING,
+                    'info' => ArcanistLintSeverity::SEVERITY_ADVICE,
+                );
+            foreach ($map as $name => $severity) {
+                    if ($severity_name == $name) {
+                            return $severity;
+            }
+        }
+        return ArcanistLintSeverity::SEVERITY_ERROR;
+    }
+}
diff --git a/.arc/src/extensions/PhpstanLinter.php b/.arc/src/extensions/PhpstanLinter.php
new file mode 100755
--- /dev/null
+++ b/.arc/src/extensions/PhpstanLinter.php
@@ -0,0 +1,242 @@
+<?php
+/**
+ * @copyright Copyright 2017-present Appsinet. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+/** Uses phpstan to lint php files */
+final class PhpstanLinter extends ArcanistExternalLinter
+{
+
+    /**
+     * @var string Config file path
+     */
+    private $configFile = null;
+
+    /**
+     * @var string Rule level
+     */
+    private $level = null;
+
+    /**
+     * @var string Autoload file path
+     */
+    private $autoloadFile = null;
+
+    public function getInfoName()
+    {
+        return 'phpstan';
+    }
+
+    public function getInfoURI()
+    {
+        return '';
+    }
+
+    public function getInfoDescription()
+    {
+        return pht('Use phpstan for processing specified files.');
+    }
+
+    public function getLinterConfigurationName()
+    {
+        return 'phpstan';
+    }
+
+    public function getDefaultBinary()
+    {
+        return 'phpstan';
+    }
+
+    public function getInstallInstructions()
+    {
+        return pht('Install phpstan following the official guide at https://github.com/phpstan/phpstan#installation');
+    }
+
+    public function shouldExpectCommandErrors()
+    {
+        return true;
+    }
+
+    public function getVersion()
+    {
+        list($stdout) = execx('%C --version', $this->getExecutableCommand());
+
+        $matches = array();
+        $regex = '/(?P<version>\d+\.\d+\.\d+)/';
+        if (preg_match($regex, $stdout, $matches)) {
+            return $matches['version'];
+        } else {
+            return false;
+        }
+    }
+
+    protected function getMandatoryFlags()
+    {
+        $flags = array(
+            'analyse',
+            '--no-progress',
+            '--error-format=checkstyle',
+            '--memory-limit=1G'
+        );
+        if (null !== $this->configFile) {
+            array_push($flags, '-c', $this->configFile);
+        }
+        if (null !== $this->level) {
+            array_push($flags, '-l', $this->level);
+        }
+        if (null !== $this->autoloadFile) {
+            array_push($flags, '-a', $this->autoloadFile);
+        }
+
+        return $flags;
+    }
+
+    public function getLinterConfigurationOptions()
+    {
+        $options = array(
+            'config' => array(
+                'type' => 'optional string',
+                'help' => pht(
+                    'The path to your phpstan.neon file. Will be provided as -c <path> to phpstan.'
+                ),
+            ),
+            'level' => array(
+                'type' => 'optional string',
+                'help' => pht(
+                    'Rule level used (0 loosest - max strictest). Will be provided as -l <level> to phpstan.'
+                ),
+            ),
+            'autoload' => array(
+                'type' => 'optional string',
+                'help' => pht(
+                    'The path to the auto load file. Will be provided as -a <autoload_file> to phpstan.'),
+            ),
+        );
+        return $options + parent::getLinterConfigurationOptions();
+    }
+
+    public function setLinterConfigurationValue($key, $value)
+    {
+        switch ($key) {
+        case 'config':
+            $this->configFile = $value;
+            return;
+        case 'level':
+            $this->level = $value;
+            return;
+        case 'autoload':
+            $this->autoloadFile = $value;
+            return;
+        default:
+            parent::setLinterConfigurationValue($key, $value);
+            return;
+        }
+    }
+
+    protected function getDefaultMessageSeverity($code)
+    {
+        return ArcanistLintSeverity::SEVERITY_WARNING;
+    }
+
+    protected function parseLinterOutput($path, $err, $stdout, $stderr)
+    {
+        $result = array();
+        if (!empty($stdout)) {
+            $stdout = substr($stdout, strpos($stdout, '<?xml'));
+            $checkstyleOutpout = new SimpleXMLElement($stdout);
+            $errors = $checkstyleOutpout->xpath('//file/error');
+            foreach($errors as $error) {
+                    $violation = $this->parseViolation($error);
+                    $violation['path'] = $path;
+                    $result[] = ArcanistLintMessage::newFromDictionary($violation);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Checkstyle returns output of the form
+     *
+     * <checkstyle>
+     *   <file name="${sPath}">
+     *     <error line="12" column="10" severity="${sSeverity}" message="${sMessage}" source="${sSource}">
+     *     ...
+     *   </file>
+     * </checkstyle>
+     *
+     * Of this, we need to extract
+    *   - Line
+     *   - Column
+     *   - Severity
+     *   - Message
+     *   - Source (name)
+     *
+     * @param SimpleXMLElement $violation The XML Entity containing the issue
+     *
+     * @return array of the form
+     * [
+     *   'line' => {int},
+    *   'column' => {int},
+     *   'severity' => {string},
+     *   'message' => {string}
+     * ]
+     */
+    private function parseViolation(SimpleXMLElement $violation)
+    {
+        return array(
+            'code' => $this->getLinterName(),
+            'name' => (string)$violation['message'],
+            'line' => (int)$violation['line'],
+            'char' => (int)$violation['column'],
+            'severity' => $this->getMatchSeverity((string)$violation['severity']),
+            'description' => (string)$violation['message']
+        );
+    }
+
+    /**
+     * @return string Linter name
+     */
+    public function getLinterName()
+    {
+        return 'phpstan';
+    }
+
+    /**
+     * Map the regex matching groups to a message severity. We look for either
+     * a nonempty severity name group like 'error', or a group called 'severity'
+     * with a valid name.
+     *
+     * @param string $severity_name dict Captured groups from regex.
+     *
+     * @return string @{class:ArcanistLintSeverity} constant.
+     *
+     * @task parse
+     */
+    private function getMatchSeverity($severity_name)
+    {
+            $map = array(
+                    'error' => ArcanistLintSeverity::SEVERITY_ERROR,
+                    'warning' => ArcanistLintSeverity::SEVERITY_WARNING,
+                    'info' => ArcanistLintSeverity::SEVERITY_ADVICE,
+                );
+            foreach ($map as $name => $severity) {
+                    if ($severity_name == $name) {
+                            return $severity;
+            }
+        }
+        return ArcanistLintSeverity::SEVERITY_ERROR;
+    }
+}
diff --git a/.arcconfig b/.arcconfig
--- a/.arcconfig
+++ b/.arcconfig
@@ -1,3 +1,4 @@
 {
-    "phabricator.uri": "https://git.kolab.org"
+    "phabricator.uri": "https://git.kolab.org",
+    "load": [".arc/arc-phpstan"]
 }
diff --git a/.arclint b/.arclint
new file mode 100644
--- /dev/null
+++ b/.arclint
@@ -0,0 +1,21 @@
+{
+  "linters": {
+    "php": {
+      "type": "php"
+    },
+    "phpcs": {
+      "type": "phpcs",
+      "bin": "src/vendor/bin/phpcs",
+      "include": "(\\.php$)",
+      "exclude": "(^.arc/)"
+    },
+    "phpstan": {
+      "type": "phpstan",
+      "include": "(\\.php$)",
+      "exclude": "(^.arc/)",
+      "config": "src/phpstan.neon",
+      "bin": "src/vendor/bin/phpstan",
+      "level": "4"
+    }
+  }
+}