diff --git a/BigBite/Docs/Classes/StringableStandard.xml b/BigBite/Docs/Classes/StringableStandard.xml
new file mode 100644
index 0000000..dc46959
--- /dev/null
+++ b/BigBite/Docs/Classes/StringableStandard.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ implements Stringable {
+ public function __toString(): string {
+ return __CLASS__;
+ }
+}
+ ]]>
+
+
+ {
+ public function __toString(): string {
+ return __CLASS__;
+ }
+}
+ ]]>
+
+
+
diff --git a/BigBite/Sniffs/Classes/StringableSniff.php b/BigBite/Sniffs/Classes/StringableSniff.php
new file mode 100644
index 0000000..8888732
--- /dev/null
+++ b/BigBite/Sniffs/Classes/StringableSniff.php
@@ -0,0 +1,107 @@
+
+ */
+ public $supportedTokenizers = array(
+ 'PHP',
+ );
+
+ /**
+ * Returns an array of tokens this test wants to listen for.
+ *
+ * @return array
+ */
+ public function register() {
+ if ( version_compare( phpversion(), '8.0.0', '<' ) ) {
+ return array();
+ }
+
+ return array( \T_FUNCTION );
+ }
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token
+ * in the stack passed in $tokens.
+ *
+ * @return void
+ */
+ public function process( File $phpcsFile, $stackPtr ) {
+ $tokens = $phpcsFile->getTokens();
+ $fnName = $phpcsFile->findNext( T_STRING, $stackPtr, ( $stackPtr + 5 ), false, null, true );
+
+ if ( false === $fnName || '__toString' !== $tokens[ $fnName ]['content'] ) {
+ return;
+ }
+
+ $classDecl = $phpcsFile->findPrevious( T_CLASS, $stackPtr, 0 );
+
+ // this __toString function isn't a class member?
+ if ( false === $classDecl ) {
+ return;
+ }
+
+ $interfaces = ObjectDeclarations::findImplementedInterfaceNames( $phpcsFile, $classDecl );
+
+ // we're good - class containing __toString implements the interface.
+ if ( is_array( $interfaces ) && in_array( 'Stringable', $interfaces, true ) ) {
+ return;
+ }
+
+ $message = 'Classes that declare "__toString" should implement the Stringable interface.';
+ $doWeFix = $phpcsFile->addFixableError( $message, $classDecl, 'NotImplemented', array(), 0 );
+
+ if ( true !== $doWeFix ) {
+ return;
+ }
+
+ $openingCurly = $tokens[ $classDecl ]['scope_opener'];
+
+ if ( $tokens[ $openingCurly ]['line'] === $tokens[ $classDecl ]['line'] ) {
+ $prevToken = $phpcsFile->findPrevious( T_STRING, $openingCurly, $classDecl );
+
+ if ( false === $prevToken ) {
+ return;
+ }
+
+ $phpcsFile->fixer->beginChangeset();
+ $phpcsFile->fixer->addContent( $prevToken, ' implements Stringable' );
+ $phpcsFile->fixer->endChangeset();
+
+ return;
+ }
+
+ $endOfLine = $phpcsFile->findPrevious( T_WHITESPACE, $openingCurly, $classDecl );
+
+ if ( false === $endOfLine ) {
+ return;
+ }
+
+ $phpcsFile->fixer->beginChangeset();
+ $phpcsFile->fixer->addContentBefore( $endOfLine, ' implements Stringable' );
+ $phpcsFile->fixer->endChangeset();
+ }
+}
diff --git a/BigBite/Tests/Classes/StringableUnitTest.1.inc b/BigBite/Tests/Classes/StringableUnitTest.1.inc
new file mode 100644
index 0000000..b15eae2
--- /dev/null
+++ b/BigBite/Tests/Classes/StringableUnitTest.1.inc
@@ -0,0 +1,29 @@
+
+ */
+ public function getErrorList( $testFile = '' ) {
+ if ( version_compare( phpversion(), '8.0.0', '<' ) ) {
+ return array();
+ }
+
+ switch ( $testFile ) {
+ case 'StringableUnitTest.1.inc':
+ return array(
+ 12 => 1,
+ 18 => 1,
+ 24 => 1,
+ );
+ default:
+ return array();
+ }
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of warnings that should occur on that line.
+ *
+ * @param string $testFile The name of the file being tested.
+ *
+ * @return array
+ */
+ public function getWarningList( $testFile = '' ) {
+ return array();
+ }
+}