2
\$\begingroup\$

This is a very simple class to handle files.

It allows to access, create and modify files in the system or 2 fake files (one in memory and other similar to /dev/null or nul).

Windows has a 'feature' that automatically converts all the line endings to the correct one. That is one of the features included.

The fake /dev/null is simply a file that works similarly to the 'real deal'.

All reads to it succeed, all writes success, using seek on it succeeds but its size is always 0.

<?

    namespace IO;

    final class InvalidFilename extends \Exception {
        function __construct( $filename ){
            $this->message = 'Invalid filename: "' . $filename . '"';
        }
    }
    final class InvalidMode extends \Exception {
        function __construct( $mode ){
            $this->message = 'Invalid mode: "' . $mode . '"';
        }
    }
    final class WriteError extends \Exception {
        function __construct( $filename ){
            $this->message = 'Could not write into "' . $filename . '"';
        }
    }
    final class ReadError extends \Exception {
        function __construct( $filename ){
            $this->message = 'Could not read from "' . $filename . '"';
        }
    }
    final class EOF extends \Exception {
        function __construct( $filename ){
            $this->message = 'End Of File "' . $filename . '"';
        }
    }

    final class File {

        const READ = 1;
        const WRITE = 2;
        const BINARY = 4;
        const MEMORY = "\1";
        const VOID = "\0";

        const EOL = PHP_EOL;
        const EOL_WIN = "\r\n";
        const EOL_LINUX = "\n";
        const EOL_OLD_MAC = "\r";

        private $info=array(
            'content'=>'',
            'path'=>'',
            'name'=>'',
            'length'=>0,
            'p'=>0,
            'mode'=>1
        );

        function __construct( $filename, $mode = 1 ){

            $this->info['mode'] = $mode & 7;
            if( !$this->info['mode'] )
            {
                throw new InvalidMode( $mode );
            }

            if( $filename != self::MEMORY && $filename != self::VOID )
            {

                $this->info['path'] = dirname( $filename );
                if( !$this->info['path'] )
                {
                    $this->info['path'] = getcwd();
                }

                $this->info['name'] = basename( $filename );
                if( !$this->info['path'] )
                {
                    throw new InvalidFilename( $filename );
                }

                if( $mode & self::READ )
                {
                    $this->info['content'] = file_get_contents( $filename );

                    if( $this->info['content'] === false)
                    {
                        throw new ReadError( $filename );
                    }

                    if( !( $mode & self::BINARY ) )
                    {
                        $this->fix_eol();
                    }

                    $this->info['length'] = strlen( $this->info['content'] );
                }

            }
            else
            {
                $this->info['path'] = $this->info['name'] = $filename;
            }
        }

        function fix_eol()
        {
            $this->info['content'] = str_replace( array( self::EOL_WIN, self::EOL_LINUX, self::EOL_OLD_MAC ), self::EOL, $this->info['content'] );
        }

        function eof()
        {
            switch( $this->info['name'] )
            {
                case self::VOID:
                    return false;
                default:
                    return $this->info['p'] >= $this->info['length'];
            }
        }

        function read( $bytes = -1 )
        {
            if( $this->info['name'] == self::VOID )
            {
                return '';
            }

            if( $this->info['name'] != self::MEMORY && !( $this->info['mode'] & self::READ ) )
            {
                throw new ReadError( $this->info['name'] );
            }

            if( $this->eof() )
            {
                throw new EOF( $this->info['name'] );
            }

            if( $bytes < 0 || $bytes > $this->info['length'] - $this->info['p'] )
            {
                $bytes = $this->info['length'] - $this->info['p'];
            }

            $p = $this->info['p'];
            $this->info['p'] += $bytes;

            return '' . substr( $this->info['content'], $p, $bytes );
        }
        function readln( $trim = true )
        {
            if( $this->info['name'] == self::VOID )
            {
                return '';
            }

            if( $this->info['name'] != self::MEMORY && !( $this->info['mode'] & self::READ ) )
            {
                throw new ReadError( $this->info['name'] );
            }

            if( $this->eof() )
            {
                throw new EOF( $this->info['name'] );
            }

            $line_end = strpos( $this->info['content'], self::EOL, $this->info['p'] );
            $p = $this->info['p'];

            if( $line_end === false )
            {
                $this->info['p'] = $this->info['length'];
                return '' . substr( $this->info['content'], $p );
            }
            else
            {
                $this->info['p'] = $line_end + strlen( self::EOL );
                return '' . substr( $this->info['content'], $p, $trim ? $line_end : $this->info['p'] );
            }
        }
        function read_bytes( $bytes )
        {
            $data = $this->read( $bytes );
            $return = array();

            for( $i = 0, $l = strlen( $data ); $i < $l; $i++ )
            {
                $return[] = ord( $data[$i] );
            }

            return $return;
        }

        function write( $data )
        {
            if( $this->info['name'] != self::VOID )
            {
                if( $this->info['name'] != self::MEMORY && !( $this->info['mode'] & self::WRITE ) )
                {
                    throw new WriteError( $this->info['name'] );
                }

                list($begin,$end) = array( substr( $this->info['content'],0, $this->info['p'] ), substr( $this->info['content'], $this->info['p'] ) );

                $this->info['p'] = strlen( $begin . $data );

                $this->info['content'] = $begin . $data . $end;

                if( !( $this->info['mode'] & self::BINARY ) )
                {
                    $this->fix_eol();
                }

                $this->info['length'] = strlen( $this->info['content'] );
            }

            return $this;
        }
        function writeln( $data )
        {
            return $this->write( $data . self::EOL );
        }
        function write_bytes()
        {
            $data = '';
            foreach( func_get_args() as $byte )
            {
                $data .= chr( $byte );
            }
            return $this->write( $data );
        }

        function seek( $byte )
        {
            if( $this->info['name'] != self::VOID && is_numeric( $byte ) )
            {
                if( $byte >= $this->info['length'] )
                {
                    $this->info['p'] = $this->info['length'];
                }
                else if( $byte >= 0)
                {
                    $this->info['p'] = (int)$byte;
                }
                else
                {
                    $this->info['p'] = $this->info['length'] - 1;
                }
            }

            return $this;
        }
        function start()
        {
            return $this->seek( 0 );
        }
        function end()
        {
            return $this->seek( -1 );
        }

        function pos()
        {
            return $this->info['p'];
        }

        function size()
        {
            return $this->info['length'];
        }

        function save( $return = false )
        {
            if( $this->info['name'] == self::VOID || $this->info['name'] == self::MEMORY )
            {
                return !!$return;
            }
            else
            {
                return file_put_contents( $this->info['path'] . DIRECTORY_SEPARATOR . $this->info['name'], $this->info['content'] );
            }
        }

        function destroy()
        {
            $this->info = array(
                'content'=>'',
                'path'=>'',
                'name'=>'',
                'length'=>0,
                'p'=>0,
                'mode'=>1
            );
        }
    }

It is quite lengthy.

Here's an example of usage:

$file = new IO\File(File::MEMORY);

//Should output 12345
echo $file->writeln(123)->start()->writeln(12345)->start()->readln(true);

In terms of usability and readability, what else should I change?

Notice the lack of atomic changes and blocking. That wasn't the aim. The aim was to get the content, change it and then save it to the file (if it is a 'real' file).

\$\endgroup\$
  • \$\begingroup\$ @Quill Sorry, I've only noticed that now. I usually start function with the curly bracket on the same line, but on loops it is in a new line. Probably a bad habit I got from my boss when writting CSS. But I've noticed that it isn't following always that style. \$\endgroup\$ – Ismael Miguel Apr 14 '15 at 22:52
  • \$\begingroup\$ Why do you have an info array with six keys instead of six private variables? \$\endgroup\$ – bdsl Aug 2 '17 at 23:20
  • \$\begingroup\$ @bdsl I have no idea at this point. Both work properly, but maybe separate variables would be better. \$\endgroup\$ – Ismael Miguel Aug 3 '17 at 0:49
1
\$\begingroup\$

Each class belongs in an own file

It seems to be overhead but remember: Each class has one task to solve and the same goes for files. In small projects it does not matter that much but as soon as a project grows it helps a lot. My namespaces are similiar to the path of the class location. As of this I know where every class is located - everytime I instantiate a class.

Namespaces

If you have not read yourself into namespaces I recommend it. Namespaces combined with an autoloader are a great way to make your daily developer life a lot easier. When using an autoloader you have another reason for having one class in one file.

further aspects

  • In function save of class File you wrote return !!$return I guess there's one ! too much.
\$\endgroup\$
  • \$\begingroup\$ Well, I agree about the namespaces, but a class per file isn't it too much? And woouldn't the code be worst since I have to look around where I'm declaring the exception? And on the save method, I think it should be actually return true;. I don't remember... (Add there: comment the code!). About the namespace, I totally agree! And that would be the best option. Thank you for the idea! \$\endgroup\$ – Ismael Miguel Apr 14 '15 at 19:47
  • \$\begingroup\$ "If you have not read yourself into namespaces I recommend it." --> 2nd line... Autoloaders are a bad idea since they make the code even harder to read and debug. When you get the WSOD (White Screen Of Death), you won't know which one of the 6 files is failing. \$\endgroup\$ – Ismael Miguel Apr 14 '15 at 20:16
  • \$\begingroup\$ Oh my bad. Well, if I experience a blank page I will take a look into the error log of the php application. In case it's empty or does not help I take a look in the error log of the apache server. Edit: An application should make usage of log files. \$\endgroup\$ – AMartinNo1 Apr 14 '15 at 20:22
  • \$\begingroup\$ Yes, it should, but in a production server you don't want those since a small mistake (using $array when undeclared or $array[a] instead of $array['a']) or by mistake leaving error_reporting(0) behind or even having the error log disabled on the php.ini loaded by php without you knowing or any other reason might cause the white page without an error log. \$\endgroup\$ – Ismael Miguel Apr 14 '15 at 20:35
  • \$\begingroup\$ You're right this should not happen but it's not recommend to develop on a production server anyway. Well, ifI have to I will enable error reporting only for my ip before making further steps. So even if I forget to remove it users will not see any errors but the blank page. \$\endgroup\$ – AMartinNo1 Apr 14 '15 at 20:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.