diff --git a/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml b/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml
new file mode 100644
index 0000000..a80ae0d
--- /dev/null
+++ b/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+ final readonly class Foo {}
+abstract readonly class Bar {}
+ ]]>
+
+
+ readonly final class Foo {}
+readonly abstract class Bar {}
+ ]]>
+
+
+
diff --git a/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php b/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php
new file mode 100644
index 0000000..234515d
--- /dev/null
+++ b/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php
@@ -0,0 +1,219 @@
+getTokens();
+ $valid = Collections::classModifierKeywords() + Tokens::$emptyTokens;
+ $classProp = [
+ 'abstract_token' => false,
+ 'final_token' => false,
+ 'readonly_token' => false,
+ ];
+
+ for ($i = ($stackPtr - 1); $i > 0; $i--) {
+ if (isset($valid[$tokens[$i]['code']]) === false) {
+ break;
+ }
+
+ switch ($tokens[$i]['code']) {
+ case \T_ABSTRACT:
+ $classProp['abstract_token'] = $i;
+ break;
+
+ case \T_FINAL:
+ $classProp['final_token'] = $i;
+ break;
+
+ case \T_READONLY:
+ $classProp['readonly_token'] = $i;
+ break;
+ }
+ }
+
+ if ($classProp['readonly_token'] === false
+ || ($classProp['final_token'] === false && $classProp['abstract_token'] === false)
+ ) {
+ /*
+ * Either no modifier keywords found at all; or only one type of modifier
+ * keyword (abstract/final or readonly) declared, but not both. No ordering needed.
+ */
+ return;
+ }
+
+ if ($classProp['final_token'] !== false && $classProp['abstract_token'] !== false) {
+ // Parse error. Ignore.
+ return;
+ }
+
+ $readonly = $classProp['readonly_token'];
+
+ if ($classProp['final_token'] !== false) {
+ $extendability = $classProp['final_token'];
+ } else {
+ $extendability = $classProp['abstract_token'];
+ }
+
+ if ($readonly < $extendability) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::READONLY_EXTEND);
+ } else {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::EXTEND_READONLY);
+ }
+
+ $message = 'Class modifier keywords are not in the correct order. Expected: "%s", found: "%s"';
+
+ switch ($this->order) {
+ case self::READONLY_EXTEND:
+ if ($readonly < $extendability) {
+ // Order is correct. Nothing to do.
+ return;
+ }
+
+ $this->handleError($phpcsFile, $extendability, $readonly);
+ break;
+
+ case self::EXTEND_READONLY:
+ default:
+ if ($extendability < $readonly) {
+ // Order is correct. Nothing to do.
+ return;
+ }
+
+ $this->handleError($phpcsFile, $readonly, $extendability);
+ break;
+ }
+ }
+
+ /**
+ * Throw the error and potentially fix it.
+ *
+ * @since 1.0.0-alpha4
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $firstKeyword The position of the first keyword found.
+ * @param int $secondKeyword The position of the second keyword token.
+ *
+ * @return void
+ */
+ private function handleError(File $phpcsFile, $firstKeyword, $secondKeyword)
+ {
+ $tokens = $phpcsFile->getTokens();
+
+ $message = 'Class modifier keywords are not in the correct order. Expected: "%s", found: "%s"';
+ $data = [
+ $tokens[$secondKeyword]['content'] . ' ' . $tokens[$firstKeyword]['content'],
+ $tokens[$firstKeyword]['content'] . ' ' . $tokens[$secondKeyword]['content'],
+ ];
+
+ $fix = $phpcsFile->addFixableError($message, $firstKeyword, 'Incorrect', $data);
+
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ $phpcsFile->fixer->replaceToken($secondKeyword, '');
+
+ // Prevent leaving behind trailing whitespace.
+ $i = ($secondKeyword + 1);
+ while ($tokens[$i]['code'] === \T_WHITESPACE) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ $i++;
+ }
+
+ // Use the original token content as the case used for keywords is not the concern of this sniff.
+ $phpcsFile->fixer->addContentBefore($firstKeyword, $tokens[$secondKeyword]['content'] . ' ');
+
+ $phpcsFile->fixer->endChangeset();
+ }
+ }
+}
diff --git a/Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc b/Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc
new file mode 100644
index 0000000..a78f971
--- /dev/null
+++ b/Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc
@@ -0,0 +1,93 @@
+ =>
+ */
+ public function getErrorList()
+ {
+ return [
+ 44 => 1,
+ 46 => 1,
+ 48 => 1,
+ 56 => 1,
+ 71 => 1,
+ 73 => 1,
+ 86 => 1,
+ ];
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array =>
+ */
+ public function getWarningList()
+ {
+ return [];
+ }
+}