From cc99dc992e74b3574aa53ba4115400c6d14f0513 Mon Sep 17 00:00:00 2001 From: Progi1984 Date: Fri, 8 Aug 2014 17:06:29 +0200 Subject: [PATCH 1/6] #4 : Reader MSProjectExchange --- CHANGELOG.md | 8 + docs/intro.rst | 10 +- samples/Sample_02_ReadGanttProject.php | 35 +- samples/Sample_02_ReadMSProjectExchange.php | 60 +-- samples/Sample_Header.php | 22 ++ samples/resources/Sample_02.mpx | 27 ++ src/PhpProject/PhpProject.php | 2 +- src/PhpProject/Reader/MsProjectMPX.php | 363 ++++++++++++++++++ .../Tests/Reader/MsProjectMPXTest.php | 63 +++ tests/PhpProject/resources/Sample_02.mpx | 27 ++ 10 files changed, 537 insertions(+), 80 deletions(-) create mode 100644 samples/resources/Sample_02.mpx create mode 100644 src/PhpProject/Reader/MsProjectMPX.php create mode 100644 tests/PhpProject/Tests/Reader/MsProjectMPXTest.php create mode 100644 tests/PhpProject/resources/Sample_02.mpx diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7bc5d..9259b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.2.0 - Not Released +### Features +- MSProjectExchange Reader - @Progi1984 GH-4 +### Bugfix + +### Miscellaneous + + ## 0.1.0 - 2014-08-08 ### Features diff --git a/docs/intro.rst b/docs/intro.rst index 2b64e3e..a80ec97 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -41,6 +41,8 @@ Writers +---------------------------+----------------------+--------+-------+ | | Custom | | | +---------------------------+----------------------+--------+-------+ +| **Document Informations** | | | | ++---------------------------+----------------------+--------+-------+ | **Project** | Task | | ✓ | +---------------------------+----------------------+--------+-------+ | | Resource | | ✓ | @@ -57,11 +59,13 @@ Readers +---------------------------+----------------------+--------+-------+ | | Custom | | | +---------------------------+----------------------+--------+-------+ -| **Project** | Task | | | +| **Document Informations** | | ✓ | | ++---------------------------+----------------------+--------+-------+ +| **Project** | Task | ✓ | ✓ | +---------------------------+----------------------+--------+-------+ -| | Resource | | | +| | Resource | ✓ | ✓ | +---------------------------+----------------------+--------+-------+ -| | Allocation | | | +| | Allocation | ✓ | ✓ | +---------------------------+----------------------+--------+-------+ Contributing diff --git a/samples/Sample_02_ReadGanttProject.php b/samples/Sample_02_ReadGanttProject.php index ff93f34..3a76c68 100644 --- a/samples/Sample_02_ReadGanttProject.php +++ b/samples/Sample_02_ReadGanttProject.php @@ -35,38 +35,11 @@ // Tasks echo date('H:i:s') . ' Get tasks'.EOL; -$oTasks = $objPHPProject->getAllTasks(); -foreach ($oTasks as $item){ - echo 'Task : '.$item->getName().EOL; - echo ' >> Duration : '.$item->getDuration().EOL; - echo ' >> StartDate : '.date('Y-m-d', $item->getStartDate()).EOL; - echo ' >> Progress : '.$item->getProgress().EOL; - echo ' >> Resources : '.EOL; - $oTaskResources = $item->getResources(); - if(!empty($oTaskResources)){ - foreach ($oTaskResources as $itemRes){ - echo ' >>>> Resource : '.$objPHPProject->getResource($itemRes)->getTitle().EOL; - } - } - - echo ' >> SubTasks : '.EOL; - if($item->getTaskCount() > 0){ - foreach ($item->getTasks() as $itemSub){ - echo ' >>>> Task : '.$itemSub->getName().EOL; - echo ' >>>>>> Duration : '.$itemSub->getDuration().EOL; - echo ' >>>>>> StartDate : '.date('Y-m-d', $itemSub->getStartDate()).EOL; - echo ' >>>>>> Progress : '.$itemSub->getProgress().EOL; - echo ' >>>>>> Resources : '.EOL; - $oTaskResources = $itemSub->getResources(); - if(!empty($oTaskResources)){ - foreach ($oTaskResources as $itemRes){ - echo ' >>>>>>>> Resource : '.$objPHPProject->getResource($itemRes)->getTitle().EOL; - } - } - } - } +$arrTasks = $objPHPProject->getAllTasks(); + +foreach ($arrTasks as $oTask){ + echoTask($objPHPProject, $oTask); } -echo EOL; // Echo done echo date('H:i:s') . ' Done reading file.'.EOL; diff --git a/samples/Sample_02_ReadMSProjectExchange.php b/samples/Sample_02_ReadMSProjectExchange.php index f80a977..f28f4d1 100644 --- a/samples/Sample_02_ReadMSProjectExchange.php +++ b/samples/Sample_02_ReadMSProjectExchange.php @@ -7,69 +7,39 @@ // Create new PHPProject object echo date('H:i:s') . ' Create new PHPProject object'.EOL; -$objReader = IOFactory::createReader('MSProjectExchange'); -$objPHPProject = $objReader->load('02file.mpx'); +$objReader = IOFactory::createReader('MsProjectMPX'); +$objPHPProject = $objReader->load(__DIR__ .DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'Sample_02.mpx'); // Set properties -echo date('H:i:s') . ' Set properties'.EOL; -echo 'Creator >'.$objPHPProject->getProperties()->getCreator().EOL; -echo 'LastModifiedBy >'.$objPHPProject->getProperties()->getLastModifiedBy().EOL; -echo 'Title >'.$objPHPProject->getProperties()->getTitle().EOL; -echo 'Subject >'.$objPHPProject->getProperties()->getSubject().EOL; -echo 'Description >'.$objPHPProject->getProperties()->getDescription().EOL; +echo date('H:i:s') . ' Get properties'.EOL; +echo 'Creator > '.$objPHPProject->getProperties()->getCreator().EOL; +echo 'LastModifiedBy > '.$objPHPProject->getProperties()->getLastModifiedBy().EOL; +echo 'Title > '.$objPHPProject->getProperties()->getTitle().EOL; +echo 'Subject > '.$objPHPProject->getProperties()->getSubject().EOL; +echo 'Description > '.$objPHPProject->getProperties()->getDescription().EOL; echo EOL; // Add some data echo date('H:i:s') . ' Get some data'.EOL; -echo 'StartDate >'.$objPHPProject->getInformations()->getStartDate().EOL; -echo 'EndDate >'.$objPHPProject->getInformations()->getEndDate().EOL; +echo 'StartDate > '.$objPHPProject->getInformations()->getStartDate().EOL; +echo 'EndDate > '.$objPHPProject->getInformations()->getEndDate().EOL; echo EOL; // Ressources echo date('H:i:s') . ' Get ressources'.EOL; $oResources = $objPHPProject->getAllResources(); foreach ($oResources as $item){ - echo 'Resource :'.$item->getTitle().EOL; + echo 'Resource : '.$item->getTitle().EOL; } echo EOL; // Tasks echo date('H:i:s') . ' Get tasks'.EOL; -$oTasks = $objPHPProject->getAllTasks(); -foreach ($oTasks as $item){ - echo 'Task :'.$item->getName().EOL; - echo ' >> Duration :'.$item->getDuration().EOL; - echo ' >> StartDate :'.$item->getStartDate().EOL; - echo ' >> EndDate :'.$item->getEndDate().EOL; - echo ' >> Progress :'.$item->getProgress().EOL; - echo ' >> Resources :'.EOL; - $oTaskResources = $item->getResources(); - if(!empty($oTaskResources)){ - foreach ($oTaskResources as $itemRes){ - echo ' >>>> Resource :'.$objPHPProject->getResource($itemRes)->getTitle().EOL; - } - } - - echo ' >> SubTasks :'.EOL; - $oSubTasks = $item->getTasks(); - if(!empty($oSubTasks)){ - foreach ($oSubTasks as $itemSub){ - echo ' >>>> Task :'.$itemSub->getName().EOL; - echo ' >>>>>> Duration :'.$itemSub->getDuration().EOL; - echo ' >>>>>> StartDate :'.$itemSub->getStartDate().EOL; - echo ' >>>>>> EndDate :'.$itemSub->getEndDate().EOL; - echo ' >>>>>> Progress :'.$itemSub->getProgress().EOL; - echo ' >>>>>> Resources :'.EOL; - $oTaskResources = $itemSub->getResources(); - if(!empty($oTaskResources)){ - foreach ($oTaskResources as $itemRes){ - echo ' >>>>>>>> Resource :'.$objPHPProject->getResource($itemRes)->getTitle().EOL; - } - } - } - } +$arrTasks = $objPHPProject->getAllTasks(); + +foreach ($arrTasks as $oTask){ + echoTask($objPHPProject, $oTask); } -echo EOL; // Echo done echo date('H:i:s') . ' Done reading file.'.EOL; diff --git a/samples/Sample_Header.php b/samples/Sample_Header.php index 8d18534..5e3a10b 100644 --- a/samples/Sample_Header.php +++ b/samples/Sample_Header.php @@ -107,6 +107,28 @@ function getEndingNotes($writers) return $result; } +function echoTask($oPHPProject, $oTask, $level = 0) { + echo ''.str_repeat('>', 2 * $level).' Task : '.$oTask->getName().''.EOL; + echo ' '.str_repeat('>', 2 * ($level + 1)).' Duration : '.$oTask->getDuration().EOL; + echo ' '.str_repeat('>', 2 * ($level + 1)).' StartDate : '.date('Y-m-d', $oTask->getStartDate()).EOL; + echo ' '.str_repeat('>', 2 * ($level + 1)).' Progress : '.$oTask->getProgress().EOL; + echo ' '.str_repeat('>', 2 * ($level + 1)).' Resources : '.EOL; + $oTaskResources = $oTask->getResources(); + if(!empty($oTaskResources)){ + foreach ($oTaskResources as $itemRes){ + echo ' '.str_repeat('>', 2 * ($level + 2)).' Resource : '.$oPHPProject->getResourceFromIndex($itemRes)->getTitle().EOL; + } + } + echo EOL; + $level++; + if($oTask->getTaskCount() > 0){ + foreach ($oTask->getTasks() as $oSubTask){ + echoTask($oPHPProject, $oSubTask, $level); + } + } + $level--; +} + ?> <?php echo $pageTitle; ?> diff --git a/samples/resources/Sample_02.mpx b/samples/resources/Sample_02.mpx new file mode 100644 index 0000000..4e76628 --- /dev/null +++ b/samples/resources/Sample_02.mpx @@ -0,0 +1,27 @@ +MPX;Microsoft Project for Windows;4.0;ANSI +10;$;1;2;.;, +11;2;0;1;8,00;40,00;$10,00/h;$15,00/h;1;0 +12;1;0;480;/;:;AM;PM;20;0 +20;Standard;0;1;1;1;1;1;0 +25;1 +25;2;08:00 AM;12:00 PM;01:00 PM;05:00 PM +25;3;08:00 AM;12:00 PM;01:00 PM;05:00 PM +25;4;08:00 AM;12:00 PM;01:00 PM;05:00 PM +25;5;08:00 AM;12:00 PM;01:00 PM;05:00 PM +25;6;08:00 AM;12:00 PM;01:00 PM;05:00 PM +25;7 +30;Project1;;;Standard;01/01/2003;;0;05/12/2003;;$0,00;$0,00;$0,00;0h;0h;0h;0%;0d;0d;0d;0%;;;;;0d;0d +40;Name;ID;Max Units;Unique ID +41;1;40;41;49 +50;Resource1;1;;1 +50;Resource2;2;0,5;2 +60;Name;WBS;Outline Level;Duration;% Complete;Start;Actual Start;Predecessors;Fixed;ID;Constraint Type;Unique ID;Outline Number;Summary +61;1;2;3;40;44;50;58;70;80;90;91;98;99;120 +70;Summary Task;1.0;1;;;;;;No;1;As Soon As Possible;1;1.0;Yes +70;First Sub Task;1.1;2;10,5d;55,5%;01/01/2003;01/01/2003;;No;2;As Soon As Possible;2;1.1 +75;1;1;80h;;40h;;;;;;;;1 +76;;0;0;NA;NA +70;Second Sub Task;1.2;2;10d;;11/01/2003;;2;No;3;As Soon As Possible;3;1.2 +75;2;1;10d;;;;;;;;;;2 +76;;0;0;NA;NA +70;Milestone;1.3;2;0d;;21/01/2003;;3;No;4;As Soon As Possible;4;1.3 \ No newline at end of file diff --git a/src/PhpProject/PhpProject.php b/src/PhpProject/PhpProject.php index 7cd8775..c6987ee 100644 --- a/src/PhpProject/PhpProject.php +++ b/src/PhpProject/PhpProject.php @@ -109,7 +109,7 @@ public function setProperties(DocumentProperties $pValue) /** * Get informations * - * @return PHPProject_DocumentInformations + * @return DocumentInformations */ public function getInformations() { diff --git a/src/PhpProject/Reader/MsProjectMPX.php b/src/PhpProject/Reader/MsProjectMPX.php new file mode 100644 index 0000000..fd46894 --- /dev/null +++ b/src/PhpProject/Reader/MsProjectMPX.php @@ -0,0 +1,363 @@ +phpProject = new PhpProject(); + } + /** + * + * @param string $pFilename + * @return PHPProject + */ + public function canRead ($pFilename) + { + if (!file_exists($pFilename) || !is_readable($pFilename)) { + return false; + } + $sContent = file_get_contents($pFilename); + $arrayLines = explode(PHP_EOL, $sContent); + + // The only required record is the File Creation record + foreach ($arrayLines as $sLine) { + $arrayRecord = explode(';', $sLine); + if (!is_numeric($arrayRecord[0]) && $arrayRecord[0] == 'MPX') { + return true; + } + } + + return false; + } + + /** + * + * @param string $pFilename + * @throws \Exception + * @return PHPProject|null + */ + public function load ($pFilename) + { + if (!$this->canRead($pFilename)) { + throw new \Exception('The file is not accessible.'); + } + $sContent = file_get_contents($pFilename); + $arrayLines = explode(PHP_EOL, $sContent); + + foreach ($arrayLines as $sLine) { + $arrayRecord = explode(';', $sLine); + switch ($arrayRecord[0]) { + case 'MPX': // File Creation + case '10': // Currency Settings + case '11': // Default Settings + case '12': // Date and Time Settings + case '20': // Base Calendar Definition + case '25': // Base Calendar Hours + case '26': // Base Calendar Exception + break; + case '30': // Project Header + $this->readRecord30($arrayRecord); + break; + case '40': // Text Resource Table Definition + // Label for 41 + break; + case '41': // Numeric Resource Table Definition + $this->readRecord41($arrayRecord); + break; + case '50': // Resource + $this->readRecord50($arrayRecord); + break; + //case '51': // Resource Notes + //case '55': // Resource Calendar Definition + //case '56': // Resource Calendar Hours + //case '57': // Resource Calendar Exception + // break; + case '60': // Text Task Table Definition + // Label for 61 + break; + case '61': // Numeric Task Table Definition + $this->readRecord61($arrayRecord); + break; + case '70': // Task + $this->readRecord70($arrayRecord); + break; + //case '71': // Task Notes + //case '72': // Recurring Task + // break; + case '75': // Resource Assignment + $this->readRecord75($arrayRecord); + break; + case '76': // Resource Assignment Workgroup Fields + case '80': // Project Names + case '81': // DDE and OLE Client Links + case '0': // Comments + default: + // throw new \Exception('load : Not implemented ('.$arrayRecord[0].')'); + } + } + + return $this->phpProject; + } + + /** + * Project Header + * @param array $record + */ + private function readRecord30(array $record) + { + // 0 : Record + // 1 : Project tab + // 2 : Company + // 3 : Manager + // 4 : Calendar (Standard used if no entry) + // 5 : Start Date (either this field or the next field is calculated for an imported file, depending on the Schedule From setting) + if (isset($record[5]) && !empty($record[5])) { + $this->phpProject->getInformations()->setStartDate($record[5]); + } + // 6 : Finish Date + //if (isset($record[6]) && !empty($record[6])) { + // $this->phpProject->getInformations()->setEndDate($record[6]); + //} + // 7 : Schedule From (0 = start, 1 = finish) + // 8 : Current Date* + // 9 : Comments + // 10 : Cost* + // 11 : Baseline Cost + // 12 : Actual Cost + // 13 : Work + // 14 : Baseline Work + // 15 : Actual Work + // 16 : Work + // 17 : Duration + // 18 : Baseline Duration + // 19 : Actual Duration + // 20 : Percent Complete + // 21 : Baseline Start + // 22 : Baseline Finish + // 23 : Actual Start + // 24 : Actual Finish + // 25 : Start Variance + // 26 : Finish Variance + // 27 : Subject + // 28 : Author + // 29 : Keywords + } + + /** + * Numeric Resource Table Definition + * @param array $record + */ + private function readRecord41(array $record) + { + array_shift($record); + foreach ($record as $key => $item) { + switch ($item) { + case 1: // Name + $this->defResource[$key+1] = 'setTitle'; + break; + case 40: // ID + $this->defResource[$key+1] = 'setIndex'; + break; + case 41: // Max Units + break; + case 49: // Unique ID + break; + //default: + //throw new \Exception('readRecord41 : Not implemented ('.$item.')'); + } + } + } + + /** + * Resource + * @param array $record + */ + private function readRecord50(array $record) + { + $oResource = $this->phpProject->createResource(); + + foreach ($this->defResource as $key => $method) { + $oResource->{$method}($record[$key]); + } + } + + /** + * Numeric Task Table Definition + * @param array $record + */ + private function readRecord61(array $record) + { + array_shift($record); + foreach ($record as $key => $item) { + switch ($item) { + case 1: // Name + $this->defTask[$key + 1] = 'setName'; + break; + case 2: // WBS + break; + case 3: // Outline Level + break; + case 40: // Duration + $this->defTask[$key + 1] = 'setDuration'; + break; + case 44: // % Complete + $this->defTask[$key + 1] = 'setProgress'; + break; + case 50: // Start + $this->defTask[$key + 1] = 'setStartDate'; + break; + case 58: // Actual Start + break; + case 70: // Predecessors + $this->iParentTaskIdx = $key + 1; + break; + case 80: // Fixed + break; + case 90: // ID + $this->defTask[$key + 1] = 'setIndex'; + break; + case 91: // Constraint Type + break; + case 98: // Unique ID + break; + case 99: // Outline Number + break; + case 120: // Summary + break; + //default: + //throw new \Exception('readRecord41 : Not implemented ('.$item.')'); + } + } + } + + /** + * Task + * @param array $record + */ + private function readRecord70(array $record) + { + $oTask = null; + if (!is_null($this->iParentTaskIdx) && !empty($record[$this->iParentTaskIdx])) { + $oTaskParent = $this->phpProject->getTaskFromIndex($record[$this->iParentTaskIdx]); + if (is_object($oTaskParent)) { + $oTask = $oTaskParent->createTask(); + } + } + if (is_null($oTask)) { + $oTask = $this->phpProject->createTask(); + } + + foreach ($this->defTask as $key => $method) { + if ($method == 'setDuration') { + if (substr($record[$key], -1) == 'd') { + $record[$key] = intval(substr($record[$key], 0, -1)); + } + } + if ($method == 'setProgress') { + if (substr($record[$key], -1) == '%') { + $record[$key] = substr($record[$key], 0, -1); + $record[$key] = str_replace(',', '.', $record[$key]); + $record[$key] = floatval($record[$key]) / 100; + } + } + $oTask->{$method}($record[$key]); + } + $this->oPreviousTask = $oTask; + } + + /** + * Resource Assignment + * @param array $record + */ + private function readRecord75(array $record) + { + // 0 : Record + // 1 : ID + $idResource = null; + if (isset($record[1]) && !empty($record[1])) { + $idResource = $record[1]; + } + // 2 : Units + // 3 : Work + // 4 : Planned Work + // 5 : Actual Work + // 6 : Overtime Work + // 7 : Cost + // 8 : Planned Cost + // 9 : Actual Cost + // 10 : Start* + // 11 : Finish* + // 12 : Delay + // 13 : Resource Unique ID + + if (!is_null($idResource) && $this->oPreviousTask instanceof Task) { + $oResource = $this->phpProject->getResourceFromIndex($idResource); + if (!is_null($oResource)) { + $this->oPreviousTask->addResource($oResource); + } + } + } +} diff --git a/tests/PhpProject/Tests/Reader/MsProjectMPXTest.php b/tests/PhpProject/Tests/Reader/MsProjectMPXTest.php new file mode 100644 index 0000000..62b58be --- /dev/null +++ b/tests/PhpProject/Tests/Reader/MsProjectMPXTest.php @@ -0,0 +1,63 @@ +assertTrue($object->canRead($fileMPX)); + $this->assertFalse($object->canRead($fileGAN)); + $this->assertFalse($object->canRead($file404)); + } + + public function testLoad() + { + $file = PHPPROJECT_TESTS_BASE_DIR.DIRECTORY_SEPARATOR.'PhpProject'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'Sample_02.mpx'; + $object = new MsProjectMPX(); + $return = $object->load($file); + + $this->assertInstanceOf('PhpOffice\\PhpProject\\PhpProject', $return); + $this->assertEquals(2, $return->getResourceCount()); + $this->assertEquals(2, $return->getTaskCount()); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The file is not accessible. + */ + public function testLoadException() + { + $file404 = 'fileError'; + $object = new MsProjectMPX(); + $object->load($file404); + } +} diff --git a/tests/PhpProject/resources/Sample_02.mpx b/tests/PhpProject/resources/Sample_02.mpx new file mode 100644 index 0000000..4e76628 --- /dev/null +++ b/tests/PhpProject/resources/Sample_02.mpx @@ -0,0 +1,27 @@ +MPX;Microsoft Project for Windows;4.0;ANSI +10;$;1;2;.;, +11;2;0;1;8,00;40,00;$10,00/h;$15,00/h;1;0 +12;1;0;480;/;:;AM;PM;20;0 +20;Standard;0;1;1;1;1;1;0 +25;1 +25;2;08:00 AM;12:00 PM;01:00 PM;05:00 PM +25;3;08:00 AM;12:00 PM;01:00 PM;05:00 PM +25;4;08:00 AM;12:00 PM;01:00 PM;05:00 PM +25;5;08:00 AM;12:00 PM;01:00 PM;05:00 PM +25;6;08:00 AM;12:00 PM;01:00 PM;05:00 PM +25;7 +30;Project1;;;Standard;01/01/2003;;0;05/12/2003;;$0,00;$0,00;$0,00;0h;0h;0h;0%;0d;0d;0d;0%;;;;;0d;0d +40;Name;ID;Max Units;Unique ID +41;1;40;41;49 +50;Resource1;1;;1 +50;Resource2;2;0,5;2 +60;Name;WBS;Outline Level;Duration;% Complete;Start;Actual Start;Predecessors;Fixed;ID;Constraint Type;Unique ID;Outline Number;Summary +61;1;2;3;40;44;50;58;70;80;90;91;98;99;120 +70;Summary Task;1.0;1;;;;;;No;1;As Soon As Possible;1;1.0;Yes +70;First Sub Task;1.1;2;10,5d;55,5%;01/01/2003;01/01/2003;;No;2;As Soon As Possible;2;1.1 +75;1;1;80h;;40h;;;;;;;;1 +76;;0;0;NA;NA +70;Second Sub Task;1.2;2;10d;;11/01/2003;;2;No;3;As Soon As Possible;3;1.2 +75;2;1;10d;;;;;;;;;;2 +76;;0;0;NA;NA +70;Milestone;1.3;2;0d;;21/01/2003;;3;No;4;As Soon As Possible;4;1.3 \ No newline at end of file From 6d96e9ef958acb0411c0e18da491fab5b34d973d Mon Sep 17 00:00:00 2001 From: Progi1984 Date: Fri, 8 Aug 2014 17:11:14 +0200 Subject: [PATCH 2/6] #4 : Reader MSProjectExchange (Fix PHPCS) --- src/PhpProject/Reader/MsProjectMPX.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpProject/Reader/MsProjectMPX.php b/src/PhpProject/Reader/MsProjectMPX.php index fd46894..214c1fc 100644 --- a/src/PhpProject/Reader/MsProjectMPX.php +++ b/src/PhpProject/Reader/MsProjectMPX.php @@ -127,7 +127,7 @@ public function load ($pFilename) case '41': // Numeric Resource Table Definition $this->readRecord41($arrayRecord); break; - case '50': // Resource + case '50': // Resource $this->readRecord50($arrayRecord); break; //case '51': // Resource Notes @@ -256,7 +256,7 @@ private function readRecord61(array $record) case 1: // Name $this->defTask[$key + 1] = 'setName'; break; - case 2: // WBS + case 2: // WBS break; case 3: // Outline Level break; @@ -336,7 +336,7 @@ private function readRecord75(array $record) { // 0 : Record // 1 : ID - $idResource = null; + $idResource = null; if (isset($record[1]) && !empty($record[1])) { $idResource = $record[1]; } From e429db81079d8e455d0f2f07705cc1e453850fc2 Mon Sep 17 00:00:00 2001 From: Progi1984 Date: Wed, 13 Aug 2014 15:01:39 +0200 Subject: [PATCH 3/6] IMPROVED : Refactored resources management --- CHANGELOG.md | 2 ++ samples/Sample_01_Simple.php | 12 ++++---- samples/Sample_02_ReadGanttProject.php | 9 ++---- samples/Sample_Header.php | 6 ++-- src/PhpProject/PhpProject.php | 34 +---------------------- src/PhpProject/Task.php | 8 +++--- src/PhpProject/Writer/GanttProject.php | 15 ++++------ tests/PhpProject/Tests/PhpProjectTest.php | 27 ------------------ 8 files changed, 24 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9259b45..409d1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,11 @@ ## 0.2.0 - Not Released ### Features - MSProjectExchange Reader - @Progi1984 GH-4 +- MSProjectExchange Writer - @Progi1984 GH-2 ### Bugfix ### Miscellaneous +- Refactored resources management ## 0.1.0 - 2014-08-08 diff --git a/samples/Sample_01_Simple.php b/samples/Sample_01_Simple.php index cb32456..f8b924a 100644 --- a/samples/Sample_01_Simple.php +++ b/samples/Sample_01_Simple.php @@ -35,10 +35,9 @@ $objTask1->setEndDate('03-01-2012'); $objTask1->setProgress(0.5); $objTask1->addResource($objRes1); -$objTask1Res = $objTask1->getResources(); echo 'Resources "Start of the project"'.EOL; -foreach ($objTask1Res as $res){ - echo ' > '.$objPHPProject->getResource($res)->getTitle().EOL;; +foreach ($objTask1->getResources() as $oResource){ + echo ' > '.$oResource->getTitle().EOL; } $objTask2 = $objPHPProject->createTask(); @@ -51,10 +50,11 @@ $objTask21->setProgress(1); $objTask21->addResource($objRes2); $objTask21->addResource($objRes1); -$objTask21Res = $objTask21->getResources(); +$objTask21->addResource($objRes1); + echo 'Resources "Analysis Code"'.EOL; -foreach ($objTask21Res as $res){ - echo ' > '.$objPHPProject->getResource($res)->getTitle().EOL;; +foreach ($objTask21->getResources() as $oResource){ + echo ' > '.$oResource->getTitle().EOL;; } $objTask22 = $objTask2->createTask(); diff --git a/samples/Sample_02_ReadGanttProject.php b/samples/Sample_02_ReadGanttProject.php index 3a76c68..eebcd2d 100644 --- a/samples/Sample_02_ReadGanttProject.php +++ b/samples/Sample_02_ReadGanttProject.php @@ -27,17 +27,14 @@ // Ressources echo date('H:i:s') . ' Get ressources'.EOL; -$oResources = $objPHPProject->getAllResources(); -foreach ($oResources as $item){ - echo 'Resource : '.$item->getTitle().EOL; +foreach ($objPHPProject->getAllResources() as $oResource){ + echo 'Resource : '.$oResource->getTitle().EOL; } echo EOL; // Tasks echo date('H:i:s') . ' Get tasks'.EOL; -$arrTasks = $objPHPProject->getAllTasks(); - -foreach ($arrTasks as $oTask){ +foreach ($objPHPProject->getAllTasks() as $oTask){ echoTask($objPHPProject, $oTask); } diff --git a/samples/Sample_Header.php b/samples/Sample_Header.php index 5e3a10b..47b63d3 100644 --- a/samples/Sample_Header.php +++ b/samples/Sample_Header.php @@ -15,7 +15,7 @@ Autoloader::register(); // Set writers -$writers = array('GanttProject' => 'gan'/*, 'MSProjectExchange' => 'mpx'*/); +$writers = array('GanttProject' => 'gan', 'MsProjectMPX' => 'mpx'); // Return to the caller script when runs by CLI if (CLI) { @@ -115,8 +115,8 @@ function echoTask($oPHPProject, $oTask, $level = 0) { echo ' '.str_repeat('>', 2 * ($level + 1)).' Resources : '.EOL; $oTaskResources = $oTask->getResources(); if(!empty($oTaskResources)){ - foreach ($oTaskResources as $itemRes){ - echo ' '.str_repeat('>', 2 * ($level + 2)).' Resource : '.$oPHPProject->getResourceFromIndex($itemRes)->getTitle().EOL; + foreach ($oTaskResources as $oResource){ + echo ' '.str_repeat('>', 2 * ($level + 2)).' Resource : '.$oResource->getTitle().EOL; } } echo EOL; diff --git a/src/PhpProject/PhpProject.php b/src/PhpProject/PhpProject.php index c6987ee..a51fef9 100644 --- a/src/PhpProject/PhpProject.php +++ b/src/PhpProject/PhpProject.php @@ -168,7 +168,7 @@ public function getAllResources() /** * Get active resource * - * @return Resource + * @return Resource|null */ public function getActiveResource() { @@ -178,22 +178,6 @@ public function getActiveResource() return null; } - /** - * Get resource by index - * - * @param int $pIndex Resource index - * @return Resource - * @throws \Exception - */ - public function getResource($pIndex = 0) - { - if (!isset($this->resourceCollection[$pIndex])) { - throw new \Exception('Resource index is out of bounds.'); - } else { - return $this->resourceCollection[$pIndex]; - } - } - /** * Get resource from index * @@ -260,22 +244,6 @@ public function getActiveTask() return null; } - /** - * Get task by index - * - * @param int $pIndex Task index - * @return Task - * @throws \Exception - */ - public function getTask($pIndex = 0) - { - if (!isset($this->taskCollection[$pIndex])) { - throw new \Exception('Task index is out of bounds.'); - } else { - return $this->taskCollection[$pIndex]; - } - } - /** * Get task from index * diff --git a/src/PhpProject/Task.php b/src/PhpProject/Task.php index d7cd274..bbe8326 100644 --- a/src/PhpProject/Task.php +++ b/src/PhpProject/Task.php @@ -253,10 +253,10 @@ public function setIndex($value) * Add a resource used by the current task * @param PHPProject_Resource $pResource */ - public function addResource(Resource $pResource) + public function addResource(Resource $oResource) { - if (array_search($pResource->getIndex(), $this->resourceCollection) === false) { - $this->resourceCollection[] = $pResource->getIndex(); + if (!in_array($oResource, $this->resourceCollection)) { + $this->resourceCollection[] = &$oResource; } return $this; } @@ -264,7 +264,7 @@ public function addResource(Resource $pResource) /** * Returns a collection of all resources used by the task * - * @return integer[] + * @return Resource[] */ public function getResources() { diff --git a/src/PhpProject/Writer/GanttProject.php b/src/PhpProject/Writer/GanttProject.php index 3508c19..a5dd019 100644 --- a/src/PhpProject/Writer/GanttProject.php +++ b/src/PhpProject/Writer/GanttProject.php @@ -179,9 +179,8 @@ public function save ($pFilename) $oXML->endElement(); // task - $arrTasks = $this->phpProject->getAllTasks(); $iTaskIndex = 0; - foreach ($arrTasks as $oTask) { + foreach ($this->phpProject->getAllTasks() as $oTask) { $iTaskIndex = $this->writeTask($oXML, $oTask, $iTaskIndex); } @@ -192,11 +191,8 @@ public function save ($pFilename) $oXML->startElement('resources'); // resource - $arrResources = $this->phpProject->getAllResources(); - foreach ($arrResources as $oResource) { - if ($oResource instanceof \PhpOffice\PhpProject\Resource) { - $this->writeResource($oXML, $oResource); - } + foreach ($this->phpProject->getAllResources() as $oResource) { + $this->writeResource($oXML, $oResource); } // >resources @@ -328,10 +324,9 @@ private function writeTask (XMLWriter $oXML, Task $oTask, $iNbTasks) // Resources Allocations if ($oTask->getResourceCount() > 0) { - $arrResources = $oTask->getResources(); - foreach ($arrResources as $resourceIdx) { + foreach ($oTask->getResources() as $oResource) { $itmAllocation = array(); - $itmAllocation['id_res'] = $resourceIdx; + $itmAllocation['id_res'] = $oResource->getIndex(); $itmAllocation['id_task'] = $iNbTasks; $this->arrAllocations[] = $itmAllocation; } diff --git a/tests/PhpProject/Tests/PhpProjectTest.php b/tests/PhpProject/Tests/PhpProjectTest.php index 0e737f0..cd50e46 100644 --- a/tests/PhpProject/Tests/PhpProjectTest.php +++ b/tests/PhpProject/Tests/PhpProjectTest.php @@ -72,9 +72,6 @@ public function testResource() $this->assertCount(1, $object->getAllResources()); $this->assertInternalType('array', $object->getAllResources()); $this->assertInstanceOf('PhpOffice\\PhpProject\\Resource', $object->getActiveResource()); - // Get Resource - $this->assertInstanceOf('PhpOffice\\PhpProject\\Resource', $object->getResource()); - $this->assertInstanceOf('PhpOffice\\PhpProject\\Resource', $object->getResource(0)); } public function testResourceFromIndex() @@ -89,16 +86,6 @@ public function testResourceFromIndex() $this->assertNull($object->getResourceFromIndex(1)); } - /** - * @expectedException \Exception - * @expectedExceptionMessage Resource index is out of bounds. - */ - public function testResourceException() - { - $object = new PhpProject(); - $object->getResource(); - } - public function testTask() { $object = new PhpProject(); @@ -123,10 +110,6 @@ public function testTask() $this->assertEquals(1, $object->getActiveTaskIndex()); $this->assertInternalType('array', $object->getAllTasks()); $this->assertInstanceOf('PhpOffice\\PhpProject\\Task', $object->getActiveTask()); - // Get Task - $this->assertInstanceOf('PhpOffice\\PhpProject\\Task', $object->getTask()); - $this->assertInstanceOf('PhpOffice\\PhpProject\\Task', $object->getTask(0)); - $this->assertInstanceOf('PhpOffice\\PhpProject\\Task', $object->getTask(1)); // Active Task $this->assertInstanceOf('PhpOffice\\PhpProject\\Task', $object->setActiveTaskIndex(0)); $this->assertEquals(0, $object->getActiveTaskIndex()); @@ -155,16 +138,6 @@ public function testTaskFromIndex() $this->assertNull($object->getTaskFromIndex(1)); } - /** - * @expectedException \Exception - * @expectedExceptionMessage Task index is out of bounds. - */ - public function testTaskException() - { - $object = new PhpProject(); - $object->getTask(); - } - /** * @expectedException \Exception * @expectedExceptionMessage Task index is out of bounds. From 7db273007575832998232557a0140588f32634cc Mon Sep 17 00:00:00 2001 From: Progi1984 Date: Wed, 13 Aug 2014 15:02:00 +0200 Subject: [PATCH 4/6] #2 Writer MSProjectExchange --- docs/intro.rst | 6 +- docs/references.rst | 4 +- src/PhpProject/Writer/MsProjectMPX.php | 272 ++++++++++++++++++ .../Tests/Writer/MsProjectMPXTest.php | 95 ++++++ 4 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 src/PhpProject/Writer/MsProjectMPX.php create mode 100644 tests/PhpProject/Tests/Writer/MsProjectMPXTest.php diff --git a/docs/intro.rst b/docs/intro.rst index a80ec97..63b6b1c 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -43,11 +43,11 @@ Writers +---------------------------+----------------------+--------+-------+ | **Document Informations** | | | | +---------------------------+----------------------+--------+-------+ -| **Project** | Task | | ✓ | +| **Project** | Task | ✓ | ✓ | +---------------------------+----------------------+--------+-------+ -| | Resource | | ✓ | +| | Resource | ✓ | ✓ | +---------------------------+----------------------+--------+-------+ -| | Allocation | | ✓ | +| | Allocation | ✓ | ✓ | +---------------------------+----------------------+--------+-------+ Readers diff --git a/docs/references.rst b/docs/references.rst index f481c65..7efbe5e 100644 --- a/docs/references.rst +++ b/docs/references.rst @@ -3,12 +3,12 @@ References ========== -GanttProject +GanttProject (GAN) --------------------- - `Website `__ -MSProjectExchange +MSProjectExchange (MPX) --------------------- - `MSDN : Description of the MPX Project File Exchange Format `__ diff --git a/src/PhpProject/Writer/MsProjectMPX.php b/src/PhpProject/Writer/MsProjectMPX.php new file mode 100644 index 0000000..35fc56c --- /dev/null +++ b/src/PhpProject/Writer/MsProjectMPX.php @@ -0,0 +1,272 @@ +phpProject = $phpProject; + } + + /** + * + * @param string $pFilename + * @throws Exception + */ + public function save ($pFilename) + { + $arrProjectInfo = $this->sanitizeProject(); + + $this->writeRecordMPX(); + // Project Header + $this->writeRecord30($arrProjectInfo); + // Text Resource Table Definition + $this->writeRecord40(); + // Numeric Resource Table Definition + $this->writeRecord41(); + // Resources + foreach ($this->phpProject->getAllResources() as $oResource) { + $this->writeRecord50($oResource); + } + // Text Task Table Definition + $this->writeRecord60(); + // Numeric Task Table Definition + $this->writeRecord61(); + // Tasks + foreach ($this->phpProject->getAllTasks() as $oTask) { + $this->writeRecord70($oTask); + } + + // Writing XML Object in file + // Open file + if (file_exists($pFilename) && !is_writable($pFilename)) { + throw new \Exception("Could not open file $pFilename for writing."); + } + $fileHandle = fopen($pFilename, 'wb+'); + // Write Content + fwrite($fileHandle, implode(PHP_EOL, $this->fileContent)); + // Close file + fclose($fileHandle); + } + + /** + * @return multitype:Ambigous + */ + private function sanitizeProject () + { + // Info Project + $minDate = 0; + // Browse all tasks + $arrTasks = $this->phpProject->getAllTasks(); + foreach ($arrTasks as $oTask) { + if ($oTask->getTaskCount() == 0) { + $this->sanitizeTask($oTask); + } else { + $this->sanitizeTaskParent($oTask); + } + $tStartDate = $oTask->getStartDate(); + if ($minDate == 0 || $tStartDate < $minDate) { + $minDate = $tStartDate; + } + } + // Browse all Resources + $numResource = 0; + foreach ($this->phpProject->getAllResources() as $oResource) { + $oResource->setIndex($numResource); + $numResource++; + } + + return array( + 'date_start' => (int)$minDate + ); + } + + /** + * Permits to clean a task + * - If the duration is not filled, but the end date is, we calculate it. + * - If the end date is not filled, but the duration is, we calculate it. + * @param PHPProject_Task $oTask + */ + private function sanitizeTask (Task $oTask) + { + $pDuration = $oTask->getDuration(); + $pEndDate = $oTask->getEndDate(); + $pStartDate = $oTask->getStartDate(); + + if (is_null($pDuration) && !is_null($pEndDate)) { + $iTimeDiff = $pEndDate - $pStartDate; + $iNumDays = $iTimeDiff / 60 / 60 / 24; + $oTask->setDuration($iNumDays + 1); + } elseif (!is_null($pDuration) && is_null($pEndDate)) { + $oTask->setEndDate($pStartDate + ($pDuration * 24 * 60 * 60)); + } + } + + /** + * Permits to clean parent task and calculate parent data like total duration, + * date start and complete average. + * @param PHPProject_Task $oParentTask + */ + private function sanitizeTaskParent (Task $oParentTask) + { + $arrTasksChilds = $oParentTask->getTasks(); + + $iProgress = 0; + $tStartDate = null; + $tEndDate = null; + foreach ($arrTasksChilds as $oTaskChild) { + if ($oTaskChild->getTaskCount() == 0) { + $this->sanitizeTask($oTaskChild); + } else { + $this->sanitizeTaskParent($oTaskChild); + } + + $iProgress += $oTaskChild->getProgress(); + if (is_null($tStartDate)) { + $tStartDate = $oTaskChild->getStartDate(); + } elseif ($tStartDate > $oTaskChild->getStartDate()) { + $tStartDate = $oTaskChild->getStartDate(); + } + + if (is_null($tEndDate)) { + $tEndDate = $oTaskChild->getEndDate(); + } elseif ($tEndDate < $oTaskChild->getEndDate()) { + $tEndDate = $oTaskChild->getEndDate(); + } + } + $oParentTask->setProgress($iProgress / $oParentTask->getTaskCount()); + $oParentTask->setStartDate($tStartDate); + $oParentTask->setEndDate($tEndDate); + $oParentTask->setDuration((($tEndDate - $tStartDate) / 60 / 60 / 24) + 1); + } + + /** + * Record MPX + */ + private function writeRecordMPX() + { + $this->fileContent[] = 'MPX;Microsoft Project for Windows;4.0;ANSI'; + } + + /** + * Record "Project Header" + */ + private function writeRecord30(array $arrProjectInfo) + { + $this->fileContent[] = '30;Project1;;;Standard;'.date('d/m/Y', $arrProjectInfo['date_start']).';;0;'.date('d/m/Y').';;$0,00;$0,00;$0,00;0h;0h;0h;0%;0d;0d;0d;0%;;;;;0d;0d'; + } + + /** + * Record "Text Resource Table Definition" + */ + private function writeRecord40() + { + $this->fileContent[] = '40;ID;Name'; + } + + /** + * Record "Numeric Resource Table Definition" + */ + private function writeRecord41() + { + $this->fileContent[] = '41;40;1'; + } + + /** + * Record "Resource" + * @param Resource $oResource + */ + private function writeRecord50(Resource $oResource) + { + $this->fileContent[] = '50;'.$oResource->getIndex().';'.$oResource->getTitle(); + } + + /** + * Record "Text Task Table Definition" + */ + private function writeRecord60() + { + $this->fileContent[] = '60;ID;Name;Duration;% Complete;Start'; + } + + /** + * Record "Numeric Task Table Definition" + */ + private function writeRecord61() + { + $this->fileContent[] = '61;90;1;40;44;50'; + } + + /** + * Record "Task" + * @param Task $oTask + */ + private function writeRecord70(Task $oTask) + { + $this->fileContent[] = '70;'.$oTask->getIndex().';'.$oTask->getName().';'.$oTask->getDuration().'d;'.number_format($oTask->getProgress(), 1).';'.date('d/m/Y', $oTask->getStartDate()); + + foreach ($oTask->getResources() as $oResource) { + $this->writeRecord75($oResource); + } + + foreach ($oTask->getTasks() as $oSubTask) { + $this->writeRecord70($oSubTask); + } + } + + /** + * Record "Resource Assignment" + * @param Resource $oResource + */ + private function writeRecord75(Resource $oResource) + { + $this->fileContent[] = '75;'.$oResource->getIndex().';1;;;;;;;;;;;'; + } +} diff --git a/tests/PhpProject/Tests/Writer/MsProjectMPXTest.php b/tests/PhpProject/Tests/Writer/MsProjectMPXTest.php new file mode 100644 index 0000000..e7c8a34 --- /dev/null +++ b/tests/PhpProject/Tests/Writer/MsProjectMPXTest.php @@ -0,0 +1,95 @@ +createResource(); + $oResource->setTitle('ResourceTest'); + + $oTask1 = $oPHPProject->createTask(); + $oTask1->setName('Task1Test'); + $oTask1->addResource($oResource); + + $oTask1Child = $oTask1->createTask(); + $oTask1Child->setName('TaskChildTest'); + + $oTask1ChildChild1 = $oTask1Child->createTask(); + $oTask1ChildChild1->setName('TaskChildChild1Test'); + $oTask1ChildChild1->setStartDate('2014-08-07'); + $oTask1ChildChild1->setDuration(2); + $oTask1ChildChild2 = $oTask1Child->createTask(); + $oTask1ChildChild2->setName('TaskChildChild2Test'); + $oTask1ChildChild2->setStartDate('2014-08-06'); + $oTask1ChildChild2->setEndDate('2014-08-10'); + + $oTask2 = $oPHPProject->createTask(); + $oTask2->setStartDate('2014-08-07'); + $oTask2->setEndDate('2014-08-13'); + $oTask2->setName('Task2Test'); + + $xmlWriter = IOFactory::createWriter($oPHPProject, 'MsProjectMPX'); + $xmlWriter->save($fileOutput); + + $content = file_get_contents($fileOutput); + $content = explode(PHP_EOL, $content); + + foreach ($content as $line) { + $line = explode(';', $line); + + switch ($line[0]) { + case 'MPX': + $this->assertEquals('Microsoft Project for Windows', $line[1]); + break; + } + } + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not open file + */ + public function testSaveException() + { + $fileOutput = tempnam(sys_get_temp_dir(), 'PHPPROJECT'); + file_put_contents($fileOutput, 'AA'); + chmod($fileOutput, 0044); + + $oPHPProject = new PhpProject(); + + $oTask1 = $oPHPProject->createTask(); + $oTask1->setName('Task1Test'); + + $xmlWriter = IOFactory::createWriter($oPHPProject, 'MsProjectMPX'); + $xmlWriter->save($fileOutput); + } +} From e5674c9288c77331e194d3e9acedb035f9bb29ef Mon Sep 17 00:00:00 2001 From: Progi1984 Date: Wed, 13 Aug 2014 15:07:52 +0200 Subject: [PATCH 5/6] #2 Writer MSProjectExchange (Fix PHPCS) --- tests/PhpProject/Tests/Writer/MsProjectMPXTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhpProject/Tests/Writer/MsProjectMPXTest.php b/tests/PhpProject/Tests/Writer/MsProjectMPXTest.php index e7c8a34..7bc53c6 100644 --- a/tests/PhpProject/Tests/Writer/MsProjectMPXTest.php +++ b/tests/PhpProject/Tests/Writer/MsProjectMPXTest.php @@ -61,7 +61,7 @@ public function testSave() $xmlWriter->save($fileOutput); $content = file_get_contents($fileOutput); - $content = explode(PHP_EOL, $content); + $content = explode(PHP_EOL, $content); foreach ($content as $line) { $line = explode(';', $line); From 8ca187e773589d573bcbd3daf820a730484e0816 Mon Sep 17 00:00:00 2001 From: Progi1984 Date: Wed, 13 Aug 2014 17:08:06 +0200 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 409d1b8..51883dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,12 @@ # Changelog -## 0.2.0 - Not Released +## 0.2.0 - 2014-08-13 ### Features - MSProjectExchange Reader - @Progi1984 GH-4 - MSProjectExchange Writer - @Progi1984 GH-2 -### Bugfix ### Miscellaneous -- Refactored resources management +- Refactored resources management - @Progi1984 ## 0.1.0 - 2014-08-08 @@ -18,8 +17,6 @@ - GanttProject Writer - @Progi1984 GH-1 - GanttProject Reader - @Progi1984 GH-3 -### Bugfix - ### Miscellaneous - QA : Documentation - @Progi1984 GH-8 GH-12 -- QA : Unit Tests - @Progi1984 GH-12 \ No newline at end of file +- QA : Unit Tests - @Progi1984 GH-12