How can I go to the nth line without using fgets() & file()? - php

I'm actually developping a class which allow me to open a file & read it line by line.
class File
{
protected $path = null;
protected $cursor = null;
protected $lineCount = 0;
public function isOpen()
{
return !is_null($this->cursor);
}
public function open($flag = 'r')
{
if(!$this->isOpen())
$this->cursor = fopen($this->path, $flag);
}
public function getLine()
{
$this->open();
$line = fgets($this->cursor);
$this->lineCount++;
return $line;
}
public function close()
{
if($this->isOpen())
fclose($this->cursor);
}
}
For some reason, I would like the file open at the line which is described by the lineCount property. I don't how can I update the open() method for doing that.
Instead of using the line count, I can use the size from the beginning of the file in octet and use the fseek method to move the cursor at the right place. But I don't know how can I get the size of a line in octet when I call the fgets method.
Thanks

Given that a text file can have any amount of text in a line, there's no 100% method to quickly jumping to a position. Unless the text format is exactly fixed and known, you'll have to read line-by-line until you reach the line number you want.
If the file doesn't change between sessions, you can store the 'pointer' in the file using ftell() (basically how far into the file you've read), and later jump to that position via fseek(). You could also have your getLine method store the offsets as it reads each line, so you build an array of lines/offsets as you go. This'd let you jump backwards in the file to any arbitrary position. It would not, however, let you jump 'forward' into unknown parts of the file.

Related

PHP - send command line output to dynamically named files

I have an application which is built in CakePHP 3.
It uses Console Commands to execute several intensive processes in the background using cron.
The application consists of 5 individual commands:
src/Command/Stage1Command.php
src/Command/Stage2Command.php
src/Command/Stage3Command.php
src/Command/Stage4Command.php
src/Command/Stage5Command.php
These can be executed manually by running each one individually, e.g. to execute Stage1Command.php:
$ php bin/cake.php stage1
To make them run via Cron, I created a 6th command (src/Command/RunAllCommand.php) which goes through these in order.
// src/Command/RunAllCommand.php
class RunAllCommand extends Command
{
public function execute(Arguments $args, ConsoleIo $io)
{
$stage1 = new Step1Command();
$this->executeCommand($stage1);
// ...
$stage5 = new Stage5Command();
$this->executeCommand($stage5);
}
}
This works fine so I can now execute everything with 1 command, php bin/cake.php run_all, which will be added as a cron task to automate running the 5 processes.
The problem I'm having is that each of the 5 commands (Stage1Command ... Stage5Command) produces output which appears on standard output in the console.
I need to be able to write the output produced by each of the 5 commands individually into dynamically named files.
So I can't do something like this
$ php bin/cake.php run_all > output.log
Because
output.log would contain everything, i.e. the output from all 5 commands.
output.log isn't a dynamic filename, it has been entered manually on the command line (or as the output destination of the cron task).
I looked at Redirecting PHP output to a text file and tried the following.
Added ob_start(); to RunAllCommand.php:
namespace App\Command;
ob_start();
class RunAllCommand extends Command { ... }
After executing the first task (Stage1Command) capturing ob_get_clean() to a variable called $content:
$stage1 = new Step1Command();
$this->executeCommand($stage1);
$content = ob_get_clean();
When I var_dump($content); it comes out as an empty string:
string(0) ""
But the output is still produced on the command line when executing php bin/cake.php run_all (RunAllCommand.php).
My plan for the dynamic filename was to generate it with PHP inside RunAllCommand.php, e.g.
// $id is a dynamic ID generated from a database call.
// This $id is being generated inside a foreach() loop so is different on each iteration (hence the dynamic nature of the filename).
$id = 234343;
$filename_stage1 = 'logs/stage1_' . $id . '.txt'; // e.g. "logs/stage1_234343.txt"
Then write $content to the above file, e.g.
file_put_contents($filename_stage1, $content);
So I have 2 problems:
The output is being echoed to the console, and unavailable in $content.
Assuming (1) is fixed, how to "reset" the output buffering such that I can use file_put_contents with 5 different filenames to capture the output for the relevant stage.
On each command file you could use the LogTrait then output what file is outputting before any commands to seperate what command is logging or setup the log config with different scopes to output to different files. example of outputting to the cli-debug.log file.
use Cake\Log\LogTrait;
class Stage1Command extends Command
{
use LogTrait;
public function execute(Arguments $args, ConsoleIo $io)
{
$this->log('Stage 1 Output: ', 'debug');
//do stuff
$this->log('output stage 1 stuff', 'debug');
}
}
I have two suggestions for solving your issue.
Option 1 - Using shell_exec
shell_exec returns a string of the output, so you can write it to a log file directly.
public function execute(Arguments $args, ConsoleIo $io)
{
$stage1_log = shell_exec('bin/cake stage1 arguments');
file_put_contents('stage1_dynamic_log_file.txt', $stage1_log);
$stage2_log = shell_exec('bin/cake stage2 arguments');
file_put_contents('stage2_dynamic_log_file.txt', $stage2_log);
}
Option 2 - Overwrite the ConsoleOut stream
Alternatively a more CakePHP style would be to call the command slightly differently. If you look at the contents of executeCommand() it does a few checks and then calls command->run($args, $io)
Also if you look at how the ConsoleIo is constructed, we can override the output method so instead of using php://stdout we could use a file instead, if you look at the code for ConsoleOutput it's just using normal fopen and fwrite.
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOutput;
public function execute(Arguments $args, ConsoleIo $io)
{
// File names
$id = 234343;
$filename_stage1 = 'logs/stage1_' . $id . '.txt';
// Create command object
$stage1 = new Stage1Command();
// Define output as this filename
$output = new ConsoleOutput($filename_stage1);
// Create a new ConsoleIo using this new output method
$stage1_io = new ConsoleIo($output);
// Execute passing in the ConsoleIo with text file for output
$this->executeCommand($stage1, ['arguments'], $stage1_io);
}

Stream from a temporary file, then delete when finished?

I'm writing a temporary file by running a couple of external Unix tools over a PDF file (basically I'm using QPDF and sed to alter the colour values. Don't ask.):
// Uncompress PDF using QPDF (doesn't read from stdin, so needs tempfile.)
$compressed_file_path = tempnam(sys_get_temp_dir(), 'cruciverbal');
file_put_contents($compressed_file_path, $response->getBody());
$uncompressed_file_path = tempnam(sys_get_temp_dir(), 'cruciverbal');
$command = "qpdf --qdf --object-streams=disable '$compressed_file_path' '$uncompressed_file_path'";
exec($command, $output, $return_value);
// Run through sed (could do this bit with streaming stdin/stdout)
$fixed_file_path = tempnam(sys_get_temp_dir(), 'cruciverbal');
$command = "sed s/0.298039215/0.0/g < '$uncompressed_file_path' > '$fixed_file_path'";
exec($command, $output, $return_value);
So, when this is done I'm left with a temporary file on disk at $fixed_file_path. (NB: While I could do the whole sed process streamed in-memory without a tempfile, the QPDF utility requires an actual file as input, for good reasons.)
In my existing process, I then read the whole $fixed_file_path file in as a string, delete it, and hand the string off to another class to go do things with.
I'd now like to change that last part to using a PSR-7 stream, i.e. a \Guzzle\Psr7\Stream object. I figure it'll be more memory-efficient (I might have a few of these in the air at once) and it'll need to be a stream in the end.
However, I'm not sure then how I'd delete the temporary file when the (third-party) class I'd handed the stream off to is finished with it. Is there a method of saying "...and delete that when you're finished with it"? Or auto-cleaning my temporary files in some other way, without keeping track of them manually?
I'd been vaguely considering rolling my own SelfDestructingFileStream, but that seemed like overkill and I thought I might be missing something.
Sounds like what you want is something like this:
<?php
class TempFile implements \Psr\Http\Message\StreamInterface {
private $resource;
public function __construct() {
$this->resource = tmpfile();
}
public function __destruct() {
$this->close();
}
public function getFilename() {
return $this->getMetadata('uri');
}
public function getMetadata($key = null) {
$data = stream_get_meta_data($this->resource);
if($key) {
return $data[$key];
}
return $data;
}
public function close() {
fclose($this->resource);
}
// TODO: implement methods from https://github.com/php-fig/http-message/blob/master/src/StreamInterface.php
}
Have QPDF write to $tmpFile->getFilename() and then you can pass the whole object off to your Guzzle/POST since it's PSR-7 compliant and then the file will delete itself when it goes out of scope.

move_uploaded_file() expects parameter 2 to be valid path, object given

I'm using Symfony 2.3 to save a file uploaded by a form POST.
This is the code I use in the controller:
$fileDir = '/home2/divine/Symfony/src/App/Bundle/Resources/public/files';
$form['my_file']->getData()->move($fileDir, 'book.pdf');
Under water, Symfony executes this code to move the file:
move_uploaded_file("/tmp/phpBM9kw8", "/home2/divine/Symfony/src/App/Bundle/Resources/public/files/book.pdf");
The public directory has 777 permissions.
This is the error I get:
"Could not move the file "/tmp/phpBM9kw8" to "/home2/divine/Symfony/src/App/Bundle/Resources/public/files/book.pdf"
(move_uploaded_file() expects parameter 2 to be valid path, object given)"
I'm using PHP 5.3.
Update:
This is the code snipped that executes the move_uploaded_file():
// Class: Symfony\Component\HttpFoundation\File\UploadedFile
$target = $this->getTargetFile($directory, $name);
if (!#move_uploaded_file($this->getPathname(), $target)) {
// etc...
The $target" variable is created here:
protected function getTargetFile($directory, $name = null) {
// Some error handling here...
$target = $directory.DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name));
return new File($target, false);
}
The $target variable is therefor a File class. It does have a __toString() method, inherited from SplFileInfo:
/**
* Returns the path to the file as a string
* #link http://php.net/manual/en/splfileinfo.tostring.php
* #return string the path to the file.
* #since 5.1.2
*/
public function __toString () {}
But somehow that __toString method is not working.
But somehow that __toString method is not working
It is one of the “magic methods”, it gets called automatically when the object is used in a string context – so for example if you had 'foo' . $object.
But I don’t think it is supposed to work in this situation here. Because PHP is loosely typed, you can pass anything into move_uploaded_file. No automatic conversion to string will happen at this point. And then internally, the function only checks if the parameter is a string, but doesn’t try to convert it into one – because that would make little sense, it could be any kind of object, and there is no way of telling if calling __toString would result in a valid file path.
You might wonder now, why in the error message we do get to see the path:
Could not move the file "/tmp/phpBM9kw8" to "/home2/divine/Symfony/src/App/Bundle/Resources/public/files/book.pdf"
My guess is, that when that error message is assembled, there is string concatenation going on, so that __toString does get called at this specific point.
If you are willing to modify the Symfony source code, I think this should work as an easy fix, if you just change this line
if (!#move_uploaded_file($this->getPathname(), $target)) {
to
if (!#move_uploaded_file($this->getPathname(), ''.$target)) {
– then you have the situation again, where __toString will be called, because the object is transferred into a string context by concatenating it with a string (an empty one, because we don’t want to tamper with the resulting value.)
Of course modifying a framework’s files directly is not the most recommendable way of dealing with this – after the next update, our change might be lost again. I’d recommend that you check the Symfony bugtracker (they should have something like that) to see if this is a known issue already and if maybe an official patch file exists; and otherwise report it as a bug, so that it can be fixed in a future version.

Get filesystem path from a PHP streamwrapper?

If I have a file in a php streamwrapper, such as "media://icon.png" how can I get it to tell me the filesystem path of that file, assuming it's on a filesystem?
I looked on the documentation page but didn't see a method to return the streamwrapper path.
The StreamWrapper class represents generic streams. Because not all streams are backed by the notion of a filesystem, there isn't a generic method for this.
If the uri property from stream_get_meta_data isn't working for your implementation, you can record the information during open time then access it via stream_get_meta_data. Example:
class MediaStream {
public $localpath = null;
public function stream_open($path, $mode, $options, &$opened_path)
{
$this->localpath = realpath(preg_replace('+^media://+', '/', $path));
return fopen($this->localpath, $mode, $options);
}
}
stream_wrapper_register('media', 'MediaStream');
$fp = fopen('media://tmp/icon.png', 'r');
$data = stream_get_meta_data($fp);
var_dump(
$data['wrapper_data']->localpath
);
Of course, there's always a brute force approach: after creating your resource, you can call fstat, which includes the device and inode. You can then open that device, walk its directory structure, and find that inode. See here for an example.

Get output of a Symfony command and save it to a file

I'm using Symfony 2.0.
I have created a command in Symfony and I want to take its output and write it to a file.
All I want is to take everything that is written on the standard output (on the console) and to have it in a variable. By all I mean things echoed in the command, exceptions catched in other files, called by the command and so on. I want the output both on the screen and in a variable (in order to write the content of the variable in a file). I will do the writing in the file in the end of the execute() method of the command.
Something like this:
protected function execute(InputInterface $input, OutputInterface $output)
{
// some logic and calls to services and functions
echo 'The operation was successful.';
$this->writeLogToFile($file, $output???);
}
And in the file I want to have:
[Output from the calls to other services, if any]
The operation was successful.
Can you please help me?
I tried something like this:
$stream = $output->getStream();
$content = stream_get_contents($stream, 5);
but the command doesn't finish in that way. :(
You could just forward the command output using standard shell methods with php app/console your:command > output.log. Or, if this is not an option, you could introduce a wrapper for the OutputInterface that would write to a stream and then forward calls to the wrapped output.
I needed the same thing, in my case, I wanted to email the console output for debug and audit to email, so I've made anon PHP class wrapper, which stores the line data and then passes to the original output instance, this will work only for PHP 7+.
protected function execute(InputInterface $input, OutputInterface $output) {
$loggableOutput = new class {
private $linesData;
public $output;
public function write($data) {
$this->linesData .= $data;
$this->output->write($data);
}
public function writeln($data) {
$this->linesData .= $data . "\n";
$this->output->writeln($data);
}
public function getLinesData() {
return $this->linesData;
}
};
$loggableOutput->output = $output;
//do some work with output
var_dump($loggableOutput->getLinesData());
}
Note this will only store the data written using write and writeln OutputInterface methods, this will no store any PHP warnings etc.
Sorry for bringing this up again.
I'm in a similar situation and if you browse the code for Symfony versions (2.7 onwards), there already is a solution.
You can easily adapt this to your specific problem:
// use Symfony\Component\Console\Output\BufferedOutput;
// You can use NullOutput() if you don't need the output
$output = new BufferedOutput();
$application->run($input, $output);
// return the output, don't use if you used NullOutput()
$content = $output->fetch();
This should neatly solve the problem.

Categories